diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..0d97e7b61 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,15 @@ +# Global fallback owner +* @geecoodeer + +# Core engine +/core/ @geecoodeer + +# Extensions +/extension/ @geecoodeer + +# Documentation and release assets +/docs/ @geecoodeer +/README.md @geecoodeer +/README-zh.md @geecoodeer +/CHANGELOG.md @geecoodeer +/CHANGELOG-zh.md @geecoodeer diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..109af5926 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,38 @@ +name: Bug report +description: Report a reproducible bug in SmartEngine. +title: "[Bug] " +labels: ["bug"] +body: + - type: textarea + id: problem + attributes: + label: Problem description + description: What happened and what did you expect? + validations: + required: true + - type: textarea + id: steps + attributes: + label: Reproduction steps + description: Provide minimal reproducible steps. + validations: + required: true + - type: input + id: version + attributes: + label: SmartEngine version + placeholder: e.g. 3.7.0-SNAPSHOT + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment + description: JDK, OS, DB, framework versions. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Logs / stacktrace + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..808bad882 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Question and discussion + url: https://github.com/alibaba/SmartEngine/discussions + about: Ask usage questions and discuss design choices. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..75b21f995 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,24 @@ +name: Feature request +description: Suggest an improvement or new capability. +title: "[Feature] " +labels: ["enhancement"] +body: + - type: textarea + id: motivation + attributes: + label: Motivation + description: What problem are you trying to solve? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposal + description: Describe the expected behavior and API changes. + validations: + required: true + - type: textarea + id: compatibility + attributes: + label: Compatibility impact + description: Any breaking changes, migration steps, or DB impact. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..6521f915b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,36 @@ +## Summary + +- Problem: +- Approach: + +## Change Type + +- [ ] docs +- [ ] refactor +- [ ] fix +- [ ] feature +- [ ] breaking change + +## Compatibility and Risk + +- Compatibility impact: +- Rollback plan: + +## Testing + +- [ ] Unit tests added/updated +- [ ] Integration tests added/updated +- [ ] Local verification command(s): + +## Database Impact (if any) + +- [ ] No database change +- [ ] MySQL DDL updated +- [ ] PostgreSQL DDL updated +- [ ] SQLMap updated +- [ ] Migration note included + +## Documentation + +- [ ] README/docs updated +- [ ] CHANGELOG updated diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..5f526c5d2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + branches: + - master + - dev + pull_request: + +jobs: + build: + name: Build (JDK ${{ matrix.java }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + java: [8, 17] + steps: + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ matrix.java }} + cache: maven + - name: Compile all modules + run: mvn -B -ntp -DskipTests package + + unit-test: + name: Unit tests (DB independent) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 8 + cache: maven + - name: Run core and extension tests without database dependency + run: >- + mvn -B -ntp + -pl core,extension/storage/storage-common,extension/storage/storage-custom,extension/retry/retry-common,extension/retry/retry-custom + -am test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..fdc3fe200 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,32 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 8 + cache: maven + - name: Build artifacts + run: mvn -B -ntp -DskipTests package + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: smart-engine-jars + path: | + **/target/*.jar + !**/*-sources.jar + !**/*-javadoc.jar + - name: Publish GitHub release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true diff --git a/.gitignore b/.gitignore index 2f5a9b3fa..ee5aacbad 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ release.properties #lock file using for test -smart-engine.lock \ No newline at end of file +smart-engine.lock +.factorypath diff --git a/.run/smart-engine [test].run.xml b/.run/smart-engine [test].run.xml new file mode 100644 index 000000000..1a0a34cf3 --- /dev/null +++ b/.run/smart-engine [test].run.xml @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/CHANELOG-zh.md b/CHANGELOG-zh.md similarity index 100% rename from CHANELOG-zh.md rename to CHANGELOG-zh.md diff --git a/CHANELOG.md b/CHANGELOG.md similarity index 74% rename from CHANELOG.md rename to CHANGELOG.md index 0bc101576..4bf916e6b 100644 --- a/CHANELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +# 3.7.0-SNAPSHOT +1. [Feature] Add database sharding support with index tables and Snowflake ID generator +2. [Feature] Add suspend/resume/claim APIs to SmartEngine command services +3. [Feature] Add data archive module for MySQL, Oracle, and PostgreSQL +4. [Feature] Add dynamic query support for task_instance +5. [Feature] Add chain query API for fluent query operations +6. [Feature] Enhance fluent query API with advanced filtering and pagination +7. [Feature] Add database dialect support - Oracle, DM, PostgreSQL, MySQL, OceanBase, Kingbase, SQL Server, H2 +8. [Feature] Add SmartIdTypeHandler for multiple ID type support +9. [Feature] Add NotificationQueryService and SupervisionQueryService +10. [Feature] Add NotificationCommandService and SupervisionCommandService +11. [Feature] Support dual storage mode (MEMORY + CUSTOM) +12. [Breaking Change] StorageMode.MEMORY renamed to StorageMode.CUSTOM +13. [Refactor] Deprecate old QueryService methods in favor of fluent query API +14. [Docs] Comprehensive documentation updates including API guide, architecture, and database schema +15. [Feature] Add EventBasedGateway and IntermediateCatchEvent support for event-driven branching + # 3.6.0-SNAPSHOT 1. [Feature Enhancement] Add multi-tenancy feature, thanks to @yanricheng1 for submitting the PR. 2. [Module adjustment] Remove MongoDB Storage diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a143ea1be..bb144e29e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,26 +1,57 @@ +# Contributing -## Contributing +This repository accepts contributions through pull requests to `master`. +`dev` can be used as a temporary integration branch, but changes should be merged in small, reviewable batches. -If you want to contribute to a project and make it better, your help is very welcome. Contributing is also a great way to learn more about social coding on Github, new technologies and and their ecosystems and how to make constructive, helpful bug reports, feature requests and the noblest of all contributions: a good, clean pull request. +## Prerequisites -### How to make a clean pull request +- JDK 8+ +- Maven 3.6+ +- MySQL/PostgreSQL only when running database integration tests -Look for a project's contribution instructions. If there are any, follow them. +## Build and Test -- Create a personal fork of the project on Github. -- Clone the fork on your local machine. Your remote repo on Github is called `origin`. -- Add the original repository as a remote called `upstream`. -- If you created your fork a while ago be sure to pull upstream changes into your local repository. -- Create a new branch to work on! Branch from `develop` if it exists, else from `master`. -- Implement/fix your feature, comment your code, . -- Follow the code style of the project, including indentation. -- Write or adapt tests as needed. -- Add or change the documentation as needed. -- Create a new branch if necessary. -- Push your branch to your fork on Github, the remote `origin`. -- From your fork open a pull request in the correct branch. Target the project's `develop` branch if there is one, else go for `master`! -- Wait for approval. -- Once the pull request is approved and merged you can pull the changes from `upstream` to your local repo and delete -your extra branch(es). +Run at repository root: -And last but not least: Always write your commit messages in the present tense. Your commit message should describe what the commit, when applied, does to the code – not what you did to the code. \ No newline at end of file +```bash +mvn -q -DskipTests=false test +``` + +Module-level examples: + +```bash +mvn -pl core -am test +mvn -pl extension/storage/storage-custom -am test +mvn -pl extension/storage/storage-mysql -am test +``` + +Reference: `docs/07-dev/build-and-test.md` + +## Pull Request Requirements + +- Keep each PR focused on one theme. +- Use the PR template completely, including risk and rollback notes. +- Add or update tests for behavior changes. +- Update docs for user-visible changes. +- Update `CHANGELOG.md` and `CHANGELOG-zh.md` for release-relevant changes. + +For database-related changes: + +- Update both MySQL and PostgreSQL DDL. +- Update MyBatis SQL maps. +- Provide migration notes. + +## Recommended Merge Strategy for Large Branches + +If a branch is too large to review in one PR, split into these categories: + +1. Mechanical changes (format/move/rename). +2. Build/dependency changes. +3. Feature additions. +4. Compatibility or semantic behavior changes. + +Each category should be merged only after CI passes. + +## Commit Message + +Use present tense and clearly describe the behavior change, not just the action taken. diff --git a/core/pom.xml b/core/pom.xml index 5d13e6b54..f3e04c80f 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -4,7 +4,7 @@ com.alibaba.smart.framework smart-engine - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT smart-engine-core diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/SmartEngine.java b/core/src/main/java/com/alibaba/smart/framework/engine/SmartEngine.java index 5542b65d8..e294b8b7a 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/SmartEngine.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/SmartEngine.java @@ -3,18 +3,27 @@ import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; import com.alibaba.smart.framework.engine.service.command.DeploymentCommandService; import com.alibaba.smart.framework.engine.service.command.ExecutionCommandService; +import com.alibaba.smart.framework.engine.service.command.NotificationCommandService; import com.alibaba.smart.framework.engine.service.command.ProcessCommandService; import com.alibaba.smart.framework.engine.service.command.RepositoryCommandService; +import com.alibaba.smart.framework.engine.service.command.SupervisionCommandService; import com.alibaba.smart.framework.engine.service.command.TaskCommandService; import com.alibaba.smart.framework.engine.service.command.VariableCommandService; import com.alibaba.smart.framework.engine.service.query.ActivityQueryService; import com.alibaba.smart.framework.engine.service.query.DeploymentQueryService; import com.alibaba.smart.framework.engine.service.query.ExecutionQueryService; +import com.alibaba.smart.framework.engine.service.query.NotificationQueryService; import com.alibaba.smart.framework.engine.service.query.ProcessQueryService; import com.alibaba.smart.framework.engine.service.query.RepositoryQueryService; +import com.alibaba.smart.framework.engine.service.query.SupervisionQueryService; import com.alibaba.smart.framework.engine.service.query.TaskAssigneeQueryService; import com.alibaba.smart.framework.engine.service.query.TaskQueryService; import com.alibaba.smart.framework.engine.service.query.VariableQueryService; +import com.alibaba.smart.framework.engine.query.DeploymentQuery; +import com.alibaba.smart.framework.engine.query.NotificationQuery; +import com.alibaba.smart.framework.engine.query.ProcessInstanceQuery; +import com.alibaba.smart.framework.engine.query.SupervisionQuery; +import com.alibaba.smart.framework.engine.query.TaskQuery; /** * @author 高海军 帝奇 @@ -59,6 +68,98 @@ public interface SmartEngine { TaskAssigneeQueryService getTaskAssigneeQueryService(); + SupervisionCommandService getSupervisionCommandService(); + + SupervisionQueryService getSupervisionQueryService(); + + NotificationCommandService getNotificationCommandService(); + + NotificationQueryService getNotificationQueryService(); + + // ============ Fluent Query API ============ + + /** + * Create a new task query for fluent API style querying. + * + *

Example usage: + *

{@code
+     * List tasks = smartEngine.createTaskQuery()
+     *     .processInstanceId("12345")
+     *     .taskAssignee("user001")
+     *     .taskStatus(TaskInstanceConstant.PENDING)
+     *     .orderByCreateTime().desc()
+     *     .listPage(0, 10);
+     * }
+ * + * @return a new TaskQuery instance + */ + TaskQuery createTaskQuery(); + + /** + * Create a new process instance query for fluent API style querying. + * + *

Example usage: + *

{@code
+     * List processes = smartEngine.createProcessQuery()
+     *     .processDefinitionType("approval")
+     *     .startedBy("user001")
+     *     .processStatus("running")
+     *     .orderByStartTime().desc()
+     *     .listPage(0, 10);
+     * }
+ * + * @return a new ProcessInstanceQuery instance + */ + ProcessInstanceQuery createProcessQuery(); + + /** + * Create a new supervision query for fluent API style querying. + * + *

Example usage: + *

{@code
+     * List supervisions = smartEngine.createSupervisionQuery()
+     *     .supervisorUserId("supervisor001")
+     *     .supervisionStatus("active")
+     *     .orderByCreateTime().desc()
+     *     .listPage(0, 10);
+     * }
+ * + * @return a new SupervisionQuery instance + */ + SupervisionQuery createSupervisionQuery(); + + /** + * Create a new notification query for fluent API style querying. + * + *

Example usage: + *

{@code
+     * List notifications = smartEngine.createNotificationQuery()
+     *     .receiverUserId("user001")
+     *     .readStatus("unread")
+     *     .orderByCreateTime().desc()
+     *     .listPage(0, 10);
+     * }
+ * + * @return a new NotificationQuery instance + */ + NotificationQuery createNotificationQuery(); + + /** + * Create a new deployment query for fluent API style querying. + * + *

Example usage: + *

{@code
+     * List deployments = smartEngine.createDeploymentQuery()
+     *     .processDefinitionType("approval")
+     *     .deploymentStatus(DeploymentStatusConstant.ACTIVE)
+     *     .orderByModifyTime().desc()
+     *     .listPage(0, 10);
+     * }
+ * + * @return a new DeploymentQuery instance + */ + DeploymentQuery createDeploymentQuery(); + void init(ProcessEngineConfiguration processEngineConfiguration); diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/behavior/impl/UserTaskBehavior.java b/core/src/main/java/com/alibaba/smart/framework/engine/behavior/impl/UserTaskBehavior.java index 47b46fd67..8bc8e5e21 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/behavior/impl/UserTaskBehavior.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/behavior/impl/UserTaskBehavior.java @@ -26,6 +26,7 @@ import com.alibaba.smart.framework.engine.model.instance.TaskAssigneeInstance; import com.alibaba.smart.framework.engine.model.instance.TaskInstance; import com.alibaba.smart.framework.engine.pvm.PvmActivity; +import com.alibaba.smart.framework.engine.configuration.TaskEventPublisher; import com.alibaba.smart.framework.engine.pvm.event.EventConstant; import org.slf4j.Logger; @@ -101,6 +102,9 @@ public boolean enter(ExecutionContext context, PvmActivity pvmActivity) { executionInstance.setTaskInstance(taskInstance); + // Fire TASK_ASSIGNED for each countersign task + fireTaskAssignedEvent(context, taskInstance, taskAssigneeInstanceList); + } } else { @@ -125,6 +129,9 @@ public boolean enter(ExecutionContext context, PvmActivity pvmActivity) { //2.1 普通UserTask,只会创建出一个TI和可能多个TACI(TaskAssigneeCandidateInstance) taskInstance.setTaskAssigneeInstanceList(taskAssigneeInstanceList); executionInstance.setTaskInstance(taskInstance); + + // Fire TASK_ASSIGNED for normal userTask (may have multiple assignees) + fireTaskAssignedEvent(context, taskInstance, taskAssigneeInstanceList); } } @@ -313,4 +320,37 @@ private void handleException(Integer totalInstanceCount, Integer passedTaskInsta throw new EngineException(message); } + /** + * Fire TASK_ASSIGNED via TaskEventPublisher SPI if configured. + * Does NOT use fireEvent/ListenerExecutor — TASK_* events are a separate channel + * from ACTIVITY_* events, published only through TaskEventPublisher. + * + * Called within engine transaction context. Downstream listeners should use + * @TransactionalEventListener(phase = AFTER_COMMIT) to ensure + * they only act after successful commit. + */ + private void fireTaskAssignedEvent(ExecutionContext context, + TaskInstance taskInstance, + List assignees) { + TaskEventPublisher publisher = context.getProcessEngineConfiguration().getTaskEventPublisher(); + if (publisher == null) { + return; + } + try { + Map extra = new HashMap<>(); + if (assignees != null) { + List assigneeIds = new ArrayList<>(); + for (TaskAssigneeInstance a : assignees) { + assigneeIds.add(a.getAssigneeId()); + } + extra.put("assigneeIds", assigneeIds); + } + publisher.publish(EventConstant.TASK_ASSIGNED, taskInstance, + context.getProcessInstance().getTenantId(), extra); + } catch (Exception e) { + LOGGER.warn("Failed to publish TASK_ASSIGNED event for task {}: {}", + taskInstance.getInstanceId(), e.getMessage()); + } + } + } diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/behavior/impl/UserTaskBehaviorHelper.java b/core/src/main/java/com/alibaba/smart/framework/engine/behavior/impl/UserTaskBehaviorHelper.java index c6c6f4c3d..79f8c4445 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/behavior/impl/UserTaskBehaviorHelper.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/behavior/impl/UserTaskBehaviorHelper.java @@ -3,10 +3,13 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; import com.alibaba.smart.framework.engine.SmartEngine; +import com.alibaba.smart.framework.engine.configuration.TaskEventPublisher; +import com.alibaba.smart.framework.engine.pvm.event.EventConstant; import com.alibaba.smart.framework.engine.bpmn.assembly.task.UserTask; import com.alibaba.smart.framework.engine.common.util.CollectionUtil; import com.alibaba.smart.framework.engine.common.util.MarkDoneUtil; @@ -129,6 +132,13 @@ public static void markDoneEIAndCancelTI(ExecutionContext context, ExecutionInst // 这里产生了db 读写访问, MarkDoneUtil.markDoneTaskInstance(taskInstance,TaskInstanceConstant.CANCELED,taskInstance.getStatus(),context.getRequest(),taskInstanceStorage,processEngineConfiguration); + + // Fire TASK_CANCELED + TaskEventPublisher publisher = processEngineConfiguration.getTaskEventPublisher(); + if (publisher != null) { + publisher.publish(EventConstant.TASK_CANCELED, taskInstance, + executionInstance.getTenantId(), Map.of("reason", "countersign_decision")); + } } } @@ -187,6 +197,19 @@ static void compensateExecutionAndTask(ExecutionContext context, UserTask userTa taskAssigneeInstance.setTaskInstanceId(taskInstance.getInstanceId()); taskAssigneeStorage.insert(taskAssigneeInstance, processEngineConfiguration); } + + // Fire TASK_ASSIGNED for compensated sequential countersign task + TaskEventPublisher publisher = processEngineConfiguration.getTaskEventPublisher(); + if (publisher != null) { + Map extra = new HashMap<>(); + List assigneeIds = new ArrayList<>(); + for (TaskAssigneeInstance tai : taskAssigneeInstanceList) { + assigneeIds.add(tai.getAssigneeId()); + } + extra.put("assigneeIds", assigneeIds); + publisher.publish(EventConstant.TASK_ASSIGNED, taskInstance, + activityInstance.getTenantId(), extra); + } } } } diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/bpmn/assembly/event/IntermediateCatchEvent.java b/core/src/main/java/com/alibaba/smart/framework/engine/bpmn/assembly/event/IntermediateCatchEvent.java new file mode 100644 index 000000000..d8be82183 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/bpmn/assembly/event/IntermediateCatchEvent.java @@ -0,0 +1,22 @@ +package com.alibaba.smart.framework.engine.bpmn.assembly.event; + +import javax.xml.namespace.QName; + +import com.alibaba.smart.framework.engine.bpmn.constant.BpmnNameSpaceConstant; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class IntermediateCatchEvent extends AbstractEvent { + + public final static QName qtype = new QName(BpmnNameSpaceConstant.NAME_SPACE, "intermediateCatchEvent"); + + private static final long serialVersionUID = 4528917365201847392L; + + @Override + public String toString() { + return super.getId(); + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/bpmn/assembly/event/parser/IntermediateCatchEventParser.java b/core/src/main/java/com/alibaba/smart/framework/engine/bpmn/assembly/event/parser/IntermediateCatchEventParser.java new file mode 100644 index 000000000..f67c550ee --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/bpmn/assembly/event/parser/IntermediateCatchEventParser.java @@ -0,0 +1,35 @@ +package com.alibaba.smart.framework.engine.bpmn.assembly.event.parser; + +import java.util.Map; + +import javax.xml.stream.XMLStreamReader; + +import com.alibaba.smart.framework.engine.bpmn.assembly.event.IntermediateCatchEvent; +import com.alibaba.smart.framework.engine.bpmn.assembly.process.parser.AbstractBpmnParser; +import com.alibaba.smart.framework.engine.extension.annotation.ExtensionBinding; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.xml.parser.ParseContext; +import com.alibaba.smart.framework.engine.xml.util.XmlParseUtil; + +@ExtensionBinding(group = ExtensionConstant.ELEMENT_PARSER, bindKey = IntermediateCatchEvent.class) + +public class IntermediateCatchEventParser extends AbstractBpmnParser { + + @Override + public Class getModelType() { + return IntermediateCatchEvent.class; + } + + @Override + public IntermediateCatchEvent parseModel(XMLStreamReader reader, ParseContext context) { + IntermediateCatchEvent intermediateCatchEvent = new IntermediateCatchEvent(); + intermediateCatchEvent.setId(XmlParseUtil.getString(reader, "id")); + intermediateCatchEvent.setName(XmlParseUtil.getString(reader, "name")); + + Map properties = super.parseExtendedProperties(reader, context); + intermediateCatchEvent.setProperties(properties); + + return intermediateCatchEvent; + } + +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/bpmn/assembly/gateway/EventBasedGateway.java b/core/src/main/java/com/alibaba/smart/framework/engine/bpmn/assembly/gateway/EventBasedGateway.java new file mode 100644 index 000000000..49cf620f2 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/bpmn/assembly/gateway/EventBasedGateway.java @@ -0,0 +1,23 @@ +package com.alibaba.smart.framework.engine.bpmn.assembly.gateway; + +import javax.xml.namespace.QName; + +import com.alibaba.smart.framework.engine.bpmn.constant.BpmnNameSpaceConstant; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class EventBasedGateway extends AbstractGateway { + + public final static QName qtype = new QName(BpmnNameSpaceConstant.NAME_SPACE, "eventBasedGateway"); + + private static final long serialVersionUID = 7631298475012839456L; + + @Override + public String toString() { + return super.getId(); + } + +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/bpmn/assembly/gateway/parser/EventBasedGatewayParser.java b/core/src/main/java/com/alibaba/smart/framework/engine/bpmn/assembly/gateway/parser/EventBasedGatewayParser.java new file mode 100644 index 000000000..40b27865c --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/bpmn/assembly/gateway/parser/EventBasedGatewayParser.java @@ -0,0 +1,35 @@ +package com.alibaba.smart.framework.engine.bpmn.assembly.gateway.parser; + +import java.util.Map; + +import javax.xml.stream.XMLStreamReader; + +import com.alibaba.smart.framework.engine.bpmn.assembly.gateway.EventBasedGateway; +import com.alibaba.smart.framework.engine.bpmn.assembly.process.parser.AbstractBpmnParser; +import com.alibaba.smart.framework.engine.extension.annotation.ExtensionBinding; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.xml.parser.ParseContext; +import com.alibaba.smart.framework.engine.xml.util.XmlParseUtil; + +@ExtensionBinding(group = ExtensionConstant.ELEMENT_PARSER, bindKey = EventBasedGateway.class) + +public class EventBasedGatewayParser extends AbstractBpmnParser { + + @Override + public Class getModelType() { + return EventBasedGateway.class; + } + + @Override + public EventBasedGateway parseModel(XMLStreamReader reader, ParseContext context) { + EventBasedGateway eventBasedGateway = new EventBasedGateway(); + eventBasedGateway.setId(XmlParseUtil.getString(reader, "id")); + eventBasedGateway.setName(XmlParseUtil.getString(reader, "name")); + + Map properties = super.parseExtendedProperties(reader, context); + eventBasedGateway.setProperties(properties); + + return eventBasedGateway; + } + +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/bpmn/behavior/event/IntermediateCatchEventBehavior.java b/core/src/main/java/com/alibaba/smart/framework/engine/bpmn/behavior/event/IntermediateCatchEventBehavior.java new file mode 100644 index 000000000..e8ec31b6b --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/bpmn/behavior/event/IntermediateCatchEventBehavior.java @@ -0,0 +1,88 @@ +package com.alibaba.smart.framework.engine.bpmn.behavior.event; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.alibaba.smart.framework.engine.behavior.base.AbstractActivityBehavior; +import com.alibaba.smart.framework.engine.bpmn.assembly.event.IntermediateCatchEvent; +import com.alibaba.smart.framework.engine.bpmn.assembly.gateway.EventBasedGateway; +import com.alibaba.smart.framework.engine.common.util.MarkDoneUtil; +import com.alibaba.smart.framework.engine.context.ExecutionContext; +import com.alibaba.smart.framework.engine.extension.annotation.ExtensionBinding; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.model.instance.ExecutionInstance; +import com.alibaba.smart.framework.engine.model.instance.ProcessInstance; +import com.alibaba.smart.framework.engine.pvm.PvmActivity; +import com.alibaba.smart.framework.engine.pvm.PvmTransition; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ExtensionBinding(group = ExtensionConstant.ACTIVITY_BEHAVIOR, bindKey = IntermediateCatchEvent.class) +public class IntermediateCatchEventBehavior extends AbstractActivityBehavior { + + private static final Logger LOGGER = LoggerFactory.getLogger(IntermediateCatchEventBehavior.class); + + public IntermediateCatchEventBehavior() { + super(); + } + + @Override + public boolean enter(ExecutionContext context, PvmActivity pvmActivity) { + super.enter(context, pvmActivity); + // Pause and wait for external signal + return true; + } + + @Override + public void execute(ExecutionContext context, PvmActivity pvmActivity) { + // When signaled, cancel sibling branches if upstream is EventBasedGateway + cancelSiblingBranchesIfEventBased(context, pvmActivity); + + // Continue normal execution + super.execute(context, pvmActivity); + } + + private void cancelSiblingBranchesIfEventBased(ExecutionContext context, PvmActivity pvmActivity) { + // a. Find the source node of the incoming transition + Map incomeTransitions = pvmActivity.getIncomeTransitions(); + if (incomeTransitions.size() != 1) { + return; + } + PvmActivity sourceActivity = incomeTransitions.values().iterator().next().getSource(); + + // b. Check if source is an EventBasedGateway + if (!(sourceActivity.getModel() instanceof EventBasedGateway)) { + return; + } + + // c. Collect all sibling catch event activity IDs + Set siblingActivityIds = new HashSet(); + String currentActivityId = pvmActivity.getModel().getId(); + for (PvmTransition outgoing : sourceActivity.getOutcomeTransitions().values()) { + String targetId = outgoing.getTarget().getModel().getId(); + if (!targetId.equals(currentActivityId)) { + siblingActivityIds.add(targetId); + } + } + + if (siblingActivityIds.isEmpty()) { + return; + } + + // d. Find active execution instances and cancel those on sibling branches + ProcessInstance processInstance = context.getProcessInstance(); + List activeExecutions = executionInstanceStorage.findActiveExecution( + processInstance.getInstanceId(), processInstance.getTenantId(), processEngineConfiguration); + + for (ExecutionInstance ei : activeExecutions) { + if (siblingActivityIds.contains(ei.getProcessDefinitionActivityId())) { + LOGGER.debug("EventBasedGateway: canceling sibling execution instance {} on activity {}", + ei.getInstanceId(), ei.getProcessDefinitionActivityId()); + MarkDoneUtil.markDoneExecutionInstance(ei, executionInstanceStorage, processEngineConfiguration); + } + } + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/bpmn/behavior/gateway/EventBasedGatewayBehavior.java b/core/src/main/java/com/alibaba/smart/framework/engine/bpmn/behavior/gateway/EventBasedGatewayBehavior.java new file mode 100644 index 000000000..6a0a2f882 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/bpmn/behavior/gateway/EventBasedGatewayBehavior.java @@ -0,0 +1,39 @@ +package com.alibaba.smart.framework.engine.bpmn.behavior.gateway; + +import java.util.Collection; + +import com.alibaba.smart.framework.engine.behavior.base.AbstractActivityBehavior; +import com.alibaba.smart.framework.engine.bpmn.assembly.gateway.EventBasedGateway; +import com.alibaba.smart.framework.engine.bpmn.behavior.gateway.helper.CommonGatewayHelper; +import com.alibaba.smart.framework.engine.context.ExecutionContext; +import com.alibaba.smart.framework.engine.extension.annotation.ExtensionBinding; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.pvm.PvmActivity; +import com.alibaba.smart.framework.engine.pvm.PvmTransition; +import com.alibaba.smart.framework.engine.pvm.event.EventConstant; + +@ExtensionBinding(group = ExtensionConstant.ACTIVITY_BEHAVIOR, bindKey = EventBasedGateway.class) +public class EventBasedGatewayBehavior extends AbstractActivityBehavior { + + public EventBasedGatewayBehavior() { + super(); + } + + @Override + public boolean enter(ExecutionContext context, PvmActivity pvmActivity) { + // 1. Create ActivityInstance + ExecutionInstance + super.enter(context, pvmActivity); + + // 2. Mark gateway's own execution instance as inactive + context.getExecutionInstance().setActive(false); + + // 3. Fire activity start event + fireEvent(context, pvmActivity, EventConstant.ACTIVITY_START); + + // 4. Fork: enter each outgoing branch (each target is an IntermediateCatchEvent that will pause) + Collection values = pvmActivity.getOutcomeTransitions().values(); + CommonGatewayHelper.leaveAndConcurrentlyForkIfNeeded(context, pvmActivity, values); + + return true; + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/common/util/IdConverter.java b/core/src/main/java/com/alibaba/smart/framework/engine/common/util/IdConverter.java new file mode 100644 index 000000000..a39ee832c --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/common/util/IdConverter.java @@ -0,0 +1,163 @@ +package com.alibaba.smart.framework.engine.common.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.alibaba.smart.framework.engine.exception.ValidationException; + +/** + * ID type converter utility class. + * + *

Provides unified ID type conversion between String (external API) and Long (database storage). + * Centralizes all conversion logic to eliminate scattered Long.valueOf() calls and improve error handling. + * + * @author SmartEngine Team + * @since 1.x.x + */ +public final class IdConverter { + + private IdConverter() { + // Utility class, prevent instantiation + } + + // ============ String → Long Conversion ============ + + /** + * Convert String ID to Long. + * + * @param id String ID + * @return Long ID, or null if input is null + * @throws ValidationException if ID format is invalid + */ + public static Long toLong(String id) { + if (id == null) { + return null; + } + try { + return Long.parseLong(id.trim()); + } catch (NumberFormatException e) { + throw new ValidationException("Invalid ID format: '" + id + "'. Expected a valid numeric ID."); + } + } + + /** + * Convert String ID to Long with field name for better error message. + * + * @param id String ID + * @param fieldName field name for error message + * @return Long ID, or null if input is null + * @throws ValidationException if ID format is invalid + */ + public static Long toLong(String id, String fieldName) { + if (id == null) { + return null; + } + try { + return Long.parseLong(id.trim()); + } catch (NumberFormatException e) { + throw new ValidationException( + "Invalid " + fieldName + " format: '" + id + "'. Expected a valid numeric ID."); + } + } + + /** + * Safe conversion, returns null instead of throwing exception on failure. + * + * @param id String ID + * @return Long ID, or null if input is null or invalid + */ + public static Long toLongOrNull(String id) { + if (id == null) { + return null; + } + try { + return Long.parseLong(id.trim()); + } catch (NumberFormatException e) { + return null; + } + } + + /** + * Batch convert String ID list to Long list. + * + *

Returns null if input is null (important for MyBatis condition checks), + * returns empty list if input is empty. + * + * @param ids String ID list + * @return Long ID list, null if input is null, empty list if input is empty + * @throws ValidationException if any ID format is invalid + */ + public static List toLongList(List ids) { + if (ids == null) { + return null; + } + if (ids.isEmpty()) { + return new ArrayList<>(); + } + return ids.stream() + .map(IdConverter::toLong) + .collect(Collectors.toList()); + } + + // ============ Long → String Conversion ============ + + /** + * Convert Long ID to String. + * + * @param id Long ID + * @return String ID, or null if input is null + */ + public static String toString(Long id) { + return id != null ? id.toString() : null; + } + + /** + * Batch convert Long ID list to String list. + * + * @param ids Long ID list + * @return String ID list, or empty list if input is null/empty + */ + public static List toStringList(List ids) { + if (ids == null || ids.isEmpty()) { + return new ArrayList<>(); + } + return ids.stream() + .map(IdConverter::toString) + .collect(Collectors.toList()); + } + + // ============ Validation Methods ============ + + /** + * Check if string is a valid Long ID. + * + * @param id String ID + * @return true if valid numeric ID, false otherwise + */ + public static boolean isValidLongId(String id) { + if (id == null || id.trim().isEmpty()) { + return false; + } + try { + Long.parseLong(id.trim()); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * Validate ID format, throw exception if invalid. + * + * @param id String ID + * @param fieldName field name for error message + * @throws ValidationException if ID is not null and format is invalid + */ + public static void validateLongId(String id, String fieldName) { + if (id != null && !isValidLongId(id)) { + throw new ValidationException( + "Invalid " + fieldName + ": '" + id + "' is not a valid numeric ID"); + } + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/configuration/ConfigurationOption.java b/core/src/main/java/com/alibaba/smart/framework/engine/configuration/ConfigurationOption.java index 0266c3c75..ea07b4bfc 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/configuration/ConfigurationOption.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/configuration/ConfigurationOption.java @@ -24,6 +24,8 @@ public interface ConfigurationOption { ConfigurationOption EAGER_FLUSH_ENABLED_OPTION = new EagerFlushEnabledOption(); + ConfigurationOption SHARDING_MODE_ENABLED_OPTION = new ShardingModeEnabledOption(); + boolean isEnabled(); String getId(); diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/configuration/IdGenerator.java b/core/src/main/java/com/alibaba/smart/framework/engine/configuration/IdGenerator.java index 26e68b715..4c24c309c 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/configuration/IdGenerator.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/configuration/IdGenerator.java @@ -1,5 +1,7 @@ package com.alibaba.smart.framework.engine.configuration; +import java.util.concurrent.atomic.AtomicReference; + import com.alibaba.smart.framework.engine.model.instance.Instance; /** @@ -7,5 +9,36 @@ */ public interface IdGenerator { + /** + * Global ID type configuration holder. + * Used by MyBatis TypeHandler to determine how to bind ID parameters. + */ + AtomicReference> GLOBAL_ID_TYPE = new AtomicReference<>(Long.class); + void generate(Instance instance); + + /** + * Returns the Java type of generated ID. + * + * @return Long.class for numeric IDs, String.class for UUID/string IDs + */ + default Class getIdType() { + return Long.class; + } + + /** + * Configure global ID type based on this generator's ID type. + * Should be called during engine initialization. + */ + default void configure() { + GLOBAL_ID_TYPE.set(getIdType()); + } + + /** + * Get the globally configured ID type. + * Used by MyBatis TypeHandler. + */ + static Class getGlobalIdType() { + return GLOBAL_ID_TYPE.get(); + } } diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/configuration/ProcessEngineConfiguration.java b/core/src/main/java/com/alibaba/smart/framework/engine/configuration/ProcessEngineConfiguration.java index 3a8503a3b..eab61430a 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/configuration/ProcessEngineConfiguration.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/configuration/ProcessEngineConfiguration.java @@ -7,6 +7,8 @@ import com.alibaba.smart.framework.engine.annotation.Experiment; import com.alibaba.smart.framework.engine.common.expression.evaluator.ExpressionEvaluator; import com.alibaba.smart.framework.engine.configuration.scanner.AnnotationScanner; +import com.alibaba.smart.framework.engine.dialect.Dialect; +import com.alibaba.smart.framework.engine.storage.StorageRouter; /** * @author 高海军 帝奇 2016.11.11 @@ -104,6 +106,18 @@ public interface ProcessEngineConfiguration { TaskAssigneeDispatcher getTaskAssigneeDispatcher(); + /** + * Optional extension for task lifecycle events. + * When set, the engine will publish events (TASK_ASSIGNED, TASK_COMPLETED, etc.) + * at each task state change point. + * + * @param taskEventPublisher the publisher implementation + * @since 3.7.0 + */ + default void setTaskEventPublisher(TaskEventPublisher taskEventPublisher) {} + + default TaskEventPublisher getTaskEventPublisher() { return null; } + /** * 主要用于持久化变量数据。 * @param variablePersister @@ -165,6 +179,12 @@ public interface ProcessEngineConfiguration { void setPvmActivityTaskFactory(PvmActivityTaskFactory pvmActivityTaskFactory); PvmActivityTaskFactory getPvmActivityTaskFactory(); + default StorageRouter getStorageRouter() { return null; } + default void setStorageRouter(StorageRouter storageRouter) {} + + default Dialect getDialect() { return null; } + default void setDialect(Dialect dialect) {} + // 是否要干掉 用于配置扩展,默认可以为空。设计目的是根据自己的业务需求,来自定义存储(该机制会绕过引擎自带的各种Storage机制,powerful and a little UnSafe)。。 //void setPersisterStrategy(PersisterStrategy persisterStrategy); // diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/configuration/TaskEventPublisher.java b/core/src/main/java/com/alibaba/smart/framework/engine/configuration/TaskEventPublisher.java new file mode 100644 index 000000000..a9053d6ba --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/configuration/TaskEventPublisher.java @@ -0,0 +1,29 @@ +package com.alibaba.smart.framework.engine.configuration; + +import java.util.Map; + +import com.alibaba.smart.framework.engine.model.instance.TaskInstance; +import com.alibaba.smart.framework.engine.pvm.event.EventConstant; + +/** + * SPI for publishing task lifecycle events. + * Platform implementations bridge these events to their own event bus (e.g. Spring ApplicationEvent). + * + * Similar to {@link TaskAssigneeDispatcher}, this is set on {@link ProcessEngineConfiguration} + * and invoked by the engine at task state change points. + * + * @since 3.7.0 + */ +public interface TaskEventPublisher { + + /** + * Publish a task lifecycle event. + * + * @param event the event type (TASK_ASSIGNED, TASK_COMPLETED, etc.) + * @param taskInstance the task instance involved + * @param tenantId tenant identifier + * @param extra additional context (e.g. assignees, fromUserId, toUserId, reason) + */ + void publish(EventConstant event, TaskInstance taskInstance, + String tenantId, Map extra); +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/configuration/impl/DefaultProcessEngineConfiguration.java b/core/src/main/java/com/alibaba/smart/framework/engine/configuration/impl/DefaultProcessEngineConfiguration.java index 53282110d..bbab4bd7a 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/configuration/impl/DefaultProcessEngineConfiguration.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/configuration/impl/DefaultProcessEngineConfiguration.java @@ -13,7 +13,9 @@ import com.alibaba.smart.framework.engine.configuration.impl.option.DefaultOptionContainer; import com.alibaba.smart.framework.engine.configuration.scanner.AnnotationScanner; import com.alibaba.smart.framework.engine.constant.SmartBase; +import com.alibaba.smart.framework.engine.dialect.Dialect; import com.alibaba.smart.framework.engine.extension.scanner.SimpleAnnotationScanner; +import com.alibaba.smart.framework.engine.storage.StorageRouter; import lombok.Data; import org.slf4j.Logger; @@ -48,6 +50,8 @@ public class DefaultProcessEngineConfiguration implements ProcessEngineConfigura private TaskAssigneeDispatcher taskAssigneeDispatcher; + private TaskEventPublisher taskEventPublisher; + private VariablePersister variablePersister; private MultiInstanceCounter multiInstanceCounter; @@ -68,6 +72,10 @@ public class DefaultProcessEngineConfiguration implements ProcessEngineConfigura private Map magicExtension; + private StorageRouter storageRouter; + + private Dialect dialect; + public DefaultProcessEngineConfiguration() { //说明:先默认设置一个id生成器,业务使用方可以根据自己的需要再覆盖掉这个值。 this.idGenerator = new DefaultIdGenerator(); @@ -87,6 +95,8 @@ public DefaultProcessEngineConfiguration() { optionContainer.put(ConfigurationOption.EXPRESSION_COMPILE_RESULT_CACHED_OPTION); optionContainer.put(ConfigurationOption.PROCESS_DEFINITION_MULTI_TENANT_SHARE_OPTION); + this.storageRouter = new StorageRouter(this); + buildDefaultSupportNameSpace(); } diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/configuration/impl/DefaultSmartEngine.java b/core/src/main/java/com/alibaba/smart/framework/engine/configuration/impl/DefaultSmartEngine.java index c4a0411bc..160689d90 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/configuration/impl/DefaultSmartEngine.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/configuration/impl/DefaultSmartEngine.java @@ -9,21 +9,44 @@ import com.alibaba.smart.framework.engine.configuration.scanner.ExtensionBindingResult; import com.alibaba.smart.framework.engine.extension.annotation.ExtensionBinding; import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.extension.scanner.SimpleAnnotationScanner; import com.alibaba.smart.framework.engine.hook.LifeCycleHook; +import com.alibaba.smart.framework.engine.instance.storage.ActivityInstanceStorage; +import com.alibaba.smart.framework.engine.instance.storage.ExecutionInstanceStorage; +import com.alibaba.smart.framework.engine.instance.storage.ProcessInstanceStorage; +import com.alibaba.smart.framework.engine.instance.storage.TaskAssigneeStorage; +import com.alibaba.smart.framework.engine.instance.storage.TaskInstanceStorage; +import com.alibaba.smart.framework.engine.instance.storage.VariableInstanceStorage; +import com.alibaba.smart.framework.engine.storage.StorageMode; +import com.alibaba.smart.framework.engine.storage.StorageRouter; import com.alibaba.smart.framework.engine.service.command.DeploymentCommandService; import com.alibaba.smart.framework.engine.service.command.ExecutionCommandService; +import com.alibaba.smart.framework.engine.service.command.NotificationCommandService; import com.alibaba.smart.framework.engine.service.command.ProcessCommandService; import com.alibaba.smart.framework.engine.service.command.RepositoryCommandService; +import com.alibaba.smart.framework.engine.service.command.SupervisionCommandService; import com.alibaba.smart.framework.engine.service.command.TaskCommandService; import com.alibaba.smart.framework.engine.service.command.VariableCommandService; import com.alibaba.smart.framework.engine.service.query.ActivityQueryService; import com.alibaba.smart.framework.engine.service.query.DeploymentQueryService; import com.alibaba.smart.framework.engine.service.query.ExecutionQueryService; +import com.alibaba.smart.framework.engine.service.query.NotificationQueryService; import com.alibaba.smart.framework.engine.service.query.ProcessQueryService; import com.alibaba.smart.framework.engine.service.query.RepositoryQueryService; +import com.alibaba.smart.framework.engine.service.query.SupervisionQueryService; import com.alibaba.smart.framework.engine.service.query.TaskAssigneeQueryService; import com.alibaba.smart.framework.engine.service.query.TaskQueryService; import com.alibaba.smart.framework.engine.service.query.VariableQueryService; +import com.alibaba.smart.framework.engine.query.DeploymentQuery; +import com.alibaba.smart.framework.engine.query.NotificationQuery; +import com.alibaba.smart.framework.engine.query.ProcessInstanceQuery; +import com.alibaba.smart.framework.engine.query.SupervisionQuery; +import com.alibaba.smart.framework.engine.query.TaskQuery; +import com.alibaba.smart.framework.engine.query.impl.DeploymentQueryImpl; +import com.alibaba.smart.framework.engine.query.impl.NotificationQueryImpl; +import com.alibaba.smart.framework.engine.query.impl.ProcessInstanceQueryImpl; +import com.alibaba.smart.framework.engine.query.impl.SupervisionQueryImpl; +import com.alibaba.smart.framework.engine.query.impl.TaskQueryImpl; import lombok.Getter; import lombok.Setter; @@ -44,15 +67,98 @@ public void init(ProcessEngineConfiguration processEngineConfiguration) { this.setProcessEngineConfiguration(processEngineConfiguration); processEngineConfiguration.setSmartEngine(this); + // Configure global ID type for MyBatis TypeHandler + if (processEngineConfiguration.getIdGenerator() != null) { + processEngineConfiguration.getIdGenerator().configure(); + } + AnnotationScanner annotationScanner = processEngineConfiguration.getAnnotationScanner(); annotationScanner.scan(processEngineConfiguration, ExtensionBinding.class); + // Register storage implementations to StorageRouter + initializeStorageRouter(processEngineConfiguration, annotationScanner); + Map scanResult = annotationScanner.getScanResult(); lifeCycleStarted(scanResult); } + @SuppressWarnings("unchecked") + private void initializeStorageRouter(ProcessEngineConfiguration config, AnnotationScanner scanner) { + StorageRouter storageRouter = config.getStorageRouter(); + if (storageRouter == null) { + return; + } + + Class[] storageTypes = { + ProcessInstanceStorage.class, + ExecutionInstanceStorage.class, + ActivityInstanceStorage.class, + TaskInstanceStorage.class, + TaskAssigneeStorage.class, + VariableInstanceStorage.class + }; + + Map scanResult = scanner.getScanResult(); + + boolean hasCommon = false; + boolean hasCustom = false; + + // Register "common" group storages as DATABASE mode + ExtensionBindingResult commonResult = scanResult.get(ExtensionConstant.COMMON); + if (commonResult != null) { + Map commonBindings = commonResult.getBindingMap(); + for (Class storageType : storageTypes) { + Object impl = commonBindings.get(storageType); + if (impl != null) { + storageRouter.registerStorage(StorageMode.DATABASE, (Class) storageType, impl); + hasCommon = true; + } + } + } + + // Register "custom" group storages as CUSTOM mode + ExtensionBindingResult customResult = scanResult.get(ExtensionConstant.CUSTOM); + if (customResult != null) { + Map customBindings = customResult.getBindingMap(); + for (Class storageType : storageTypes) { + Object impl = customBindings.get(storageType); + if (impl != null) { + storageRouter.registerStorage(StorageMode.CUSTOM, (Class) storageType, impl); + hasCustom = true; + } + } + } + + // When only one storage module is present, also register it as the default mode + // to ensure backward compatibility + if (hasCustom && !hasCommon) { + // Only custom module: also register as DATABASE (default mode) for compatibility + Map customBindings = customResult.getBindingMap(); + for (Class storageType : storageTypes) { + Object impl = customBindings.get(storageType); + if (impl != null) { + storageRouter.registerStorage(StorageMode.DATABASE, (Class) storageType, impl); + } + } + } else if (hasCommon && !hasCustom) { + // Only common module: also register as CUSTOM for completeness + Map commonBindings = commonResult.getBindingMap(); + for (Class storageType : storageTypes) { + Object impl = commonBindings.get(storageType); + if (impl != null) { + storageRouter.registerStorage(StorageMode.CUSTOM, (Class) storageType, impl); + } + } + } + + // Set StorageRouter to Scanner for transparent proxy + if (scanner instanceof SimpleAnnotationScanner) { + ((SimpleAnnotationScanner) scanner).setStorageRouter(storageRouter); + } + } + protected void lifeCycleStarted(Map scanResult) { for (Entry stringExtensionBindingResultEntry : scanResult.entrySet()) { ExtensionBindingResult bindingResult = stringExtensionBindingResultEntry.getValue(); @@ -144,4 +250,51 @@ public TaskAssigneeQueryService getTaskAssigneeQueryService() { return processEngineConfiguration.getAnnotationScanner().getExtensionPoint(ExtensionConstant.SERVICE,TaskAssigneeQueryService.class); } + @Override + public SupervisionCommandService getSupervisionCommandService() { + return processEngineConfiguration.getAnnotationScanner().getExtensionPoint(ExtensionConstant.SERVICE, SupervisionCommandService.class); + } + + @Override + public SupervisionQueryService getSupervisionQueryService() { + return processEngineConfiguration.getAnnotationScanner().getExtensionPoint(ExtensionConstant.SERVICE, SupervisionQueryService.class); + } + + @Override + public NotificationCommandService getNotificationCommandService() { + return processEngineConfiguration.getAnnotationScanner().getExtensionPoint(ExtensionConstant.SERVICE, NotificationCommandService.class); + } + + @Override + public NotificationQueryService getNotificationQueryService() { + return processEngineConfiguration.getAnnotationScanner().getExtensionPoint(ExtensionConstant.SERVICE, NotificationQueryService.class); + } + + // ============ Fluent Query API ============ + + @Override + public TaskQuery createTaskQuery() { + return new TaskQueryImpl(processEngineConfiguration); + } + + @Override + public ProcessInstanceQuery createProcessQuery() { + return new ProcessInstanceQueryImpl(processEngineConfiguration); + } + + @Override + public SupervisionQuery createSupervisionQuery() { + return new SupervisionQueryImpl(processEngineConfiguration); + } + + @Override + public NotificationQuery createNotificationQuery() { + return new NotificationQueryImpl(processEngineConfiguration); + } + + @Override + public DeploymentQuery createDeploymentQuery() { + return new DeploymentQueryImpl(processEngineConfiguration); + } + } diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/configuration/impl/SnowflakeIdGenerator.java b/core/src/main/java/com/alibaba/smart/framework/engine/configuration/impl/SnowflakeIdGenerator.java new file mode 100644 index 000000000..e18462d0f --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/configuration/impl/SnowflakeIdGenerator.java @@ -0,0 +1,189 @@ +package com.alibaba.smart.framework.engine.configuration.impl; + +import com.alibaba.smart.framework.engine.configuration.IdGenerator; +import com.alibaba.smart.framework.engine.model.instance.Instance; + +/** + * Snowflake-based distributed ID generator. + * + *

Bit layout (64 bits total): + *

+ *  0                   1                   2                   3
+ *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |0|                    timestamp (41 bits)                        |
+ * +-+                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                                 | nodeId(5) |  sequence (17 bits)|
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * 
+ * + *
    + *
  • 1 bit - sign (always 0, positive)
  • + *
  • 41 bits - millisecond timestamp offset from custom epoch (supports ~69 years)
  • + *
  • 5 bits - node ID (0-31, supports 32 nodes)
  • + *
  • 17 bits - sequence number per millisecond (0-131071, supports 131072 IDs/ms/node)
  • + *
+ * + *

Custom epoch: 2024-01-01 00:00:00 UTC (1704067200000L) + * + *

Clock rollback protection: tolerates up to 5ms of clock drift by busy-waiting; + * throws {@link IllegalStateException} for larger rollbacks. + * + *

Thread safety: all mutable state is protected by {@code synchronized}. + */ +public class SnowflakeIdGenerator implements IdGenerator { + + // Custom epoch: 2024-01-01 00:00:00 UTC + private static final long EPOCH = 1704067200000L; + + // Bit lengths + private static final int NODE_ID_BITS = 5; + private static final int SEQUENCE_BITS = 17; + + // Max values + private static final long MAX_NODE_ID = (1L << NODE_ID_BITS) - 1; // 31 + private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1; // 131071 + + // Shift amounts + private static final int NODE_ID_SHIFT = SEQUENCE_BITS; // 17 + private static final int TIMESTAMP_SHIFT = NODE_ID_BITS + SEQUENCE_BITS; // 22 + + // Maximum tolerable clock rollback in milliseconds + private static final long MAX_CLOCK_BACKWARD_MS = 5L; + + private final long nodeId; + private long lastTimestamp = -1L; + private long sequence = 0L; + + /** + * Create a SnowflakeIdGenerator with the specified node ID. + * + * @param nodeId node identifier, must be between 0 and 31 (inclusive) + * @throws IllegalArgumentException if nodeId is out of range + */ + public SnowflakeIdGenerator(long nodeId) { + if (nodeId < 0 || nodeId > MAX_NODE_ID) { + throw new IllegalArgumentException( + "nodeId must be between 0 and " + MAX_NODE_ID + ", got: " + nodeId); + } + this.nodeId = nodeId; + } + + /** + * Create a SnowflakeIdGenerator with default node ID 0. + */ + public SnowflakeIdGenerator() { + this(0); + } + + @Override + public Class getIdType() { + return Long.class; + } + + @Override + public void generate(Instance instance) { + long id = nextId(); + instance.setInstanceId(String.valueOf(id)); + } + + /** + * Generate the next unique snowflake ID. + * + * @return a unique 64-bit snowflake ID + * @throws IllegalStateException if clock moved backward by more than 5ms + */ + public synchronized long nextId() { + long currentTimestamp = currentTimeMillis(); + + if (currentTimestamp < lastTimestamp) { + long offset = lastTimestamp - currentTimestamp; + if (offset <= MAX_CLOCK_BACKWARD_MS) { + // Wait for the clock to catch up + try { + Thread.sleep(offset); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException( + "Interrupted while waiting for clock to catch up", e); + } + currentTimestamp = currentTimeMillis(); + if (currentTimestamp < lastTimestamp) { + throw new IllegalStateException( + "Clock moved backward by " + (lastTimestamp - currentTimestamp) + + "ms after waiting. Refusing to generate ID."); + } + } else { + throw new IllegalStateException( + "Clock moved backward by " + offset + + "ms (exceeds tolerance of " + MAX_CLOCK_BACKWARD_MS + "ms). " + + "Refusing to generate ID."); + } + } + + if (currentTimestamp == lastTimestamp) { + sequence = (sequence + 1) & MAX_SEQUENCE; + if (sequence == 0) { + // Sequence exhausted for this millisecond, wait for next ms + currentTimestamp = waitForNextMillis(lastTimestamp); + } + } else { + sequence = 0; + } + + lastTimestamp = currentTimestamp; + + long timestampOffset = currentTimestamp - EPOCH; + return (timestampOffset << TIMESTAMP_SHIFT) + | (nodeId << NODE_ID_SHIFT) + | sequence; + } + + /** + * Parse the timestamp (millis since Unix epoch) from a snowflake ID. + * + * @param id the snowflake ID + * @return the absolute timestamp in milliseconds (Unix epoch) + */ + public static long parseTimestamp(long id) { + return (id >> TIMESTAMP_SHIFT) + EPOCH; + } + + /** + * Parse the node ID from a snowflake ID. + * + * @param id the snowflake ID + * @return the node ID (0-31) + */ + public static long parseNodeId(long id) { + return (id >> NODE_ID_SHIFT) & MAX_NODE_ID; + } + + /** + * Parse the sequence number from a snowflake ID. + * + * @param id the snowflake ID + * @return the sequence number (0-131071) + */ + public static long parseSequence(long id) { + return id & MAX_SEQUENCE; + } + + /** + * Block until the system clock advances past the given timestamp. + */ + private long waitForNextMillis(long lastTs) { + long ts = currentTimeMillis(); + while (ts <= lastTs) { + ts = currentTimeMillis(); + } + return ts; + } + + /** + * Returns current time in milliseconds. Extracted for testability. + */ + long currentTimeMillis() { + return System.currentTimeMillis(); + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/configuration/impl/option/ShardingModeEnabledOption.java b/core/src/main/java/com/alibaba/smart/framework/engine/configuration/impl/option/ShardingModeEnabledOption.java new file mode 100644 index 000000000..f26f60dbe --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/configuration/impl/option/ShardingModeEnabledOption.java @@ -0,0 +1,26 @@ +package com.alibaba.smart.framework.engine.configuration.impl.option; + +import com.alibaba.smart.framework.engine.configuration.ConfigurationOption; + +/** + * Sharding mode configuration option. + * When enabled, index tables (se_user_task_index, se_user_notification_index) will be maintained, + * and queries without sharding key (processInstanceId) will throw ShardingKeyRequiredException. + */ +public class ShardingModeEnabledOption implements ConfigurationOption { + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public String getId() { + return "shardingModeEnabled"; + } + + @Override + public Object getData() { + return null; + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/constant/NotificationConstant.java b/core/src/main/java/com/alibaba/smart/framework/engine/constant/NotificationConstant.java new file mode 100644 index 000000000..ee293f2a0 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/constant/NotificationConstant.java @@ -0,0 +1,25 @@ +package com.alibaba.smart.framework.engine.constant; + +/** + * 知会通知相关常量 + * + * @author SmartEngine Team + */ +public interface NotificationConstant { + + /** + * 通知类型 + */ + interface NotificationType { + String CC = "cc"; // 抄送 + String INFORM = "inform"; // 知会 + } + + /** + * 读取状态 + */ + interface ReadStatus { + String UNREAD = "unread"; // 未读 + String READ = "read"; // 已读 + } +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/constant/SupervisionConstant.java b/core/src/main/java/com/alibaba/smart/framework/engine/constant/SupervisionConstant.java new file mode 100644 index 000000000..6748f3bf0 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/constant/SupervisionConstant.java @@ -0,0 +1,26 @@ +package com.alibaba.smart.framework.engine.constant; + +/** + * 督办相关常量 + * + * @author SmartEngine Team + */ +public interface SupervisionConstant { + + /** + * 督办类型 + */ + interface SupervisionType { + String URGE = "urge"; // 催办 + String TRACK = "track"; // 跟踪 + String REMIND = "remind"; // 提醒 + } + + /** + * 督办状态 + */ + interface SupervisionStatus { + String ACTIVE = "active"; // 活跃 + String CLOSED = "closed"; // 已关闭 + } +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/context/ExecutionContext.java b/core/src/main/java/com/alibaba/smart/framework/engine/context/ExecutionContext.java index b9c6c9762..9294da5ac 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/context/ExecutionContext.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/context/ExecutionContext.java @@ -8,6 +8,7 @@ import com.alibaba.smart.framework.engine.model.instance.ActivityInstance; import com.alibaba.smart.framework.engine.model.instance.ExecutionInstance; import com.alibaba.smart.framework.engine.model.instance.ProcessInstance; +import com.alibaba.smart.framework.engine.model.instance.TaskInstance; public interface ExecutionContext { @@ -31,6 +32,10 @@ public interface ExecutionContext { void setActivityInstance(ActivityInstance activityInstance); + TaskInstance getTaskInstance(); + + void setTaskInstance(TaskInstance taskInstance); + /* END 1 */ diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/context/impl/DefaultExecutionContext.java b/core/src/main/java/com/alibaba/smart/framework/engine/context/impl/DefaultExecutionContext.java index 5ba3c094f..425dcf8a8 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/context/impl/DefaultExecutionContext.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/context/impl/DefaultExecutionContext.java @@ -10,6 +10,7 @@ import com.alibaba.smart.framework.engine.model.instance.ActivityInstance; import com.alibaba.smart.framework.engine.model.instance.ExecutionInstance; import com.alibaba.smart.framework.engine.model.instance.ProcessInstance; +import com.alibaba.smart.framework.engine.model.instance.TaskInstance; import com.alibaba.smart.framework.engine.util.ObjectUtil; import lombok.Data; @@ -30,6 +31,8 @@ public class DefaultExecutionContext implements ExecutionContext { private ActivityInstance activityInstance; + private TaskInstance taskInstance; + private ProcessDefinition processDefinition; private ProcessEngineConfiguration processEngineConfiguration; diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/dialect/Dialect.java b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/Dialect.java new file mode 100644 index 000000000..6955407ca --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/Dialect.java @@ -0,0 +1,213 @@ +package com.alibaba.smart.framework.engine.dialect; + +/** + * Database dialect interface for handling database-specific SQL syntax. + * Implementations provide database-specific SQL generation for pagination, + * ID generation, data types, and other database-specific features. + * + * @author SmartEngine Team + */ +public interface Dialect { + + /** + * Get the dialect name (e.g., "mysql", "postgresql", "oracle"). + * + * @return the dialect name + */ + String getName(); + + /** + * Get the display name for this dialect. + * + * @return the display name + */ + String getDisplayName(); + + // ============ Pagination ============ + + /** + * Build a paginated SQL statement. + * + * @param sql the original SQL + * @param offset the offset (0-based) + * @param limit the maximum number of rows to return + * @return the paginated SQL + */ + String buildPageSql(String sql, int offset, int limit); + + /** + * Check if this dialect supports LIMIT OFFSET syntax. + * + * @return true if LIMIT OFFSET is supported + */ + boolean supportsLimitOffset(); + + // ============ Time functions ============ + + /** + * Get the SQL expression for current timestamp. + * + * @return the current timestamp expression + */ + String getCurrentTimestamp(); + + /** + * Get the SQL expression for date difference calculation. + * + * @param startColumn the start date column + * @param endColumn the end date column + * @return the date difference expression (result in days) + */ + String getDateDiff(String startColumn, String endColumn); + + // ============ Data types ============ + + /** + * Get the BIGINT type name for this database. + * + * @return the BIGINT type name + */ + String getBigintType(); + + /** + * Get the VARCHAR type name for this database. + * + * @param length the varchar length + * @return the VARCHAR type definition + */ + String getVarcharType(int length); + + /** + * Get the CLOB/TEXT type name for this database. + * + * @return the CLOB type name + */ + String getClobType(); + + /** + * Get the TIMESTAMP type name for this database. + * + * @return the TIMESTAMP type name + */ + String getTimestampType(); + + // ============ ID generation ============ + + /** + * Get the ID generation type for this database. + * + * @return the ID generation type + */ + IdGenerationType getIdGenerationType(); + + /** + * Get the SQL for getting next sequence value (for SEQUENCE type databases). + * + * @param sequenceName the sequence name + * @return the SQL expression, or null if sequences are not supported + */ + String getSequenceNextValueSql(String sequenceName); + + /** + * Get the auto-increment column definition. + * + * @return the auto-increment definition, or null if not supported + */ + String getAutoIncrementDefinition(); + + // ============ Boolean values ============ + + /** + * Get the SQL literal for boolean true. + * + * @return the true literal + */ + String getBooleanTrue(); + + /** + * Get the SQL literal for boolean false. + * + * @return the false literal + */ + String getBooleanFalse(); + + // ============ String functions ============ + + /** + * Get the string concatenation operator or function. + * + * @param expressions the expressions to concatenate + * @return the concatenation expression + */ + String concat(String... expressions); + + /** + * Get the substring function. + * + * @param column the column or expression + * @param start the start position (1-based) + * @param length the length + * @return the substring expression + */ + String substring(String column, int start, int length); + + // ============ Locking ============ + + /** + * Get the FOR UPDATE clause. + * + * @return the FOR UPDATE clause + */ + String getForUpdateClause(); + + /** + * Get the FOR UPDATE NOWAIT clause. + * + * @return the FOR UPDATE NOWAIT clause, or regular FOR UPDATE if not supported + */ + String getForUpdateNoWaitClause(); + + // ============ Other ============ + + /** + * Check if this dialect supports the given feature. + * + * @param feature the feature to check + * @return true if the feature is supported + */ + boolean supportsFeature(DialectFeature feature); + + /** + * Quote an identifier (table name, column name, etc.). + * + * @param identifier the identifier to quote + * @return the quoted identifier + */ + String quoteIdentifier(String identifier); + + // ============ JSON functions ============ + + /** + * Generate SQL to extract text value from a JSON column by key. + * + * @param column the column reference (e.g., "task.extra") + * @param key single-level JSON key (e.g., "category") + * @return SQL expression evaluating to text value + */ + String jsonExtractText(String column, String key); + + /** + * Enumeration of database features that may or may not be supported. + */ + enum DialectFeature { + LIMIT_OFFSET, + SEQUENCES, + WINDOW_FUNCTIONS, + COMMON_TABLE_EXPRESSIONS, + LATERAL_JOINS, + RETURNING_CLAUSE, + UPSERT, + JSON_FUNCTIONS, + ARRAY_TYPE + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/dialect/DialectRegistry.java b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/DialectRegistry.java new file mode 100644 index 000000000..c670b577e --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/DialectRegistry.java @@ -0,0 +1,183 @@ +package com.alibaba.smart.framework.engine.dialect; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.smart.framework.engine.dialect.impl.DmDialect; +import com.alibaba.smart.framework.engine.dialect.impl.H2Dialect; +import com.alibaba.smart.framework.engine.dialect.impl.KingbaseDialect; +import com.alibaba.smart.framework.engine.dialect.impl.MySqlDialect; +import com.alibaba.smart.framework.engine.dialect.impl.OceanBaseDialect; +import com.alibaba.smart.framework.engine.dialect.impl.OracleDialect; +import com.alibaba.smart.framework.engine.dialect.impl.PostgreSqlDialect; +import com.alibaba.smart.framework.engine.dialect.impl.SqlServerDialect; + +/** + * Registry for database dialects. + * Provides dialect lookup by name and automatic detection from JDBC URLs. + * + * @author SmartEngine Team + */ +public class DialectRegistry { + + private static final DialectRegistry INSTANCE = new DialectRegistry(); + + private final Map dialects = new ConcurrentHashMap<>(); + private Dialect defaultDialect; + + private DialectRegistry() { + // Register built-in dialects + registerBuiltInDialects(); + } + + private void registerBuiltInDialects() { + register(new MySqlDialect()); + register(new PostgreSqlDialect()); + register(new H2Dialect()); + register(new OracleDialect()); + register(new SqlServerDialect()); + register(new DmDialect()); + register(new KingbaseDialect()); + register(new OceanBaseDialect()); + + // Set MySQL as default + defaultDialect = dialects.get("mysql"); + } + + /** + * Get the singleton instance. + * + * @return the registry instance + */ + public static DialectRegistry getInstance() { + return INSTANCE; + } + + /** + * Register a dialect. + * + * @param dialect the dialect to register + */ + public void register(Dialect dialect) { + dialects.put(dialect.getName().toLowerCase(), dialect); + } + + /** + * Get a dialect by name. + * + * @param name the dialect name (case-insensitive) + * @return the dialect, or null if not found + */ + public Dialect getDialect(String name) { + if (name == null) { + return defaultDialect; + } + return dialects.get(name.toLowerCase()); + } + + /** + * Get a dialect by name, or throw if not found. + * + * @param name the dialect name + * @return the dialect + * @throws IllegalArgumentException if dialect not found + */ + public Dialect getDialectOrThrow(String name) { + Dialect dialect = getDialect(name); + if (dialect == null) { + throw new IllegalArgumentException("Unknown dialect: " + name + + ". Available dialects: " + dialects.keySet()); + } + return dialect; + } + + /** + * Detect dialect from JDBC URL. + * + * @param jdbcUrl the JDBC URL + * @return the detected dialect, or null if not detected + */ + public Dialect detectDialect(String jdbcUrl) { + if (jdbcUrl == null || jdbcUrl.isEmpty()) { + return null; + } + + String lowerUrl = jdbcUrl.toLowerCase(); + + if (lowerUrl.contains(":mysql:") || lowerUrl.contains(":mariadb:")) { + return getDialect("mysql"); + } + if (lowerUrl.contains(":postgresql:") || lowerUrl.contains(":pgsql:")) { + return getDialect("postgresql"); + } + if (lowerUrl.contains(":h2:")) { + return getDialect("h2"); + } + if (lowerUrl.contains(":oracle:")) { + return getDialect("oracle"); + } + if (lowerUrl.contains(":sqlserver:") || lowerUrl.contains(":microsoft:")) { + return getDialect("sqlserver"); + } + if (lowerUrl.contains(":dm:") || lowerUrl.contains(":dameng:")) { + return getDialect("dm"); + } + if (lowerUrl.contains(":kingbase:")) { + return getDialect("kingbase"); + } + if (lowerUrl.contains(":oceanbase:")) { + return getDialect("oceanbase"); + } + + return null; + } + + /** + * Get the default dialect. + * + * @return the default dialect + */ + public Dialect getDefaultDialect() { + return defaultDialect; + } + + /** + * Set the default dialect. + * + * @param defaultDialect the default dialect + */ + public void setDefaultDialect(Dialect defaultDialect) { + this.defaultDialect = defaultDialect; + } + + /** + * Set the default dialect by name. + * + * @param dialectName the dialect name + * @throws IllegalArgumentException if dialect not found + */ + public void setDefaultDialect(String dialectName) { + this.defaultDialect = getDialectOrThrow(dialectName); + } + + /** + * Get all registered dialects. + * + * @return unmodifiable collection of dialects + */ + public Collection getAllDialects() { + return Collections.unmodifiableCollection(dialects.values()); + } + + /** + * Check if a dialect is registered. + * + * @param name the dialect name + * @return true if registered + */ + public boolean hasDialect(String name) { + return name != null && dialects.containsKey(name.toLowerCase()); + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/dialect/IdGenerationType.java b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/IdGenerationType.java new file mode 100644 index 000000000..34bc3a6ee --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/IdGenerationType.java @@ -0,0 +1,34 @@ +package com.alibaba.smart.framework.engine.dialect; + +/** + * Enumeration of ID generation strategies supported by different databases. + * + * @author SmartEngine Team + */ +public enum IdGenerationType { + + /** + * Auto-increment column (MySQL, H2, SQLite, OceanBase) + */ + AUTO_INCREMENT, + + /** + * Identity column (PostgreSQL, SQL Server, 达梦, 人大金仓) + */ + IDENTITY, + + /** + * Sequence (Oracle) + */ + SEQUENCE, + + /** + * UUID generated by application + */ + UUID, + + /** + * Custom ID generator (e.g., Snowflake) + */ + CUSTOM +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/AbstractDialect.java b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/AbstractDialect.java new file mode 100644 index 000000000..2a0ed99df --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/AbstractDialect.java @@ -0,0 +1,82 @@ +package com.alibaba.smart.framework.engine.dialect.impl; + +import com.alibaba.smart.framework.engine.dialect.Dialect; +import com.alibaba.smart.framework.engine.dialect.IdGenerationType; + +/** + * Abstract base implementation of Dialect with common functionality. + * + * @author SmartEngine Team + */ +public abstract class AbstractDialect implements Dialect { + + @Override + public String getBigintType() { + return "BIGINT"; + } + + @Override + public String getVarcharType(int length) { + return "VARCHAR(" + length + ")"; + } + + @Override + public String getTimestampType() { + return "TIMESTAMP"; + } + + @Override + public String getBooleanTrue() { + return "TRUE"; + } + + @Override + public String getBooleanFalse() { + return "FALSE"; + } + + @Override + public String concat(String... expressions) { + // Default: use CONCAT function + return "CONCAT(" + String.join(", ", expressions) + ")"; + } + + @Override + public String substring(String column, int start, int length) { + return "SUBSTRING(" + column + ", " + start + ", " + length + ")"; + } + + @Override + public String getForUpdateClause() { + return "FOR UPDATE"; + } + + @Override + public String getForUpdateNoWaitClause() { + return "FOR UPDATE NOWAIT"; + } + + @Override + public String getSequenceNextValueSql(String sequenceName) { + // Default: not supported + return null; + } + + @Override + public String getAutoIncrementDefinition() { + // Default: not supported + return null; + } + + @Override + public String quoteIdentifier(String identifier) { + // Default: use double quotes (ANSI SQL standard) + return "\"" + identifier + "\""; + } + + @Override + public String jsonExtractText(String column, String key) { + // Default: Oracle/DM/SQL Server syntax + return "JSON_VALUE(" + column + ", '$." + key + "')"; + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/DmDialect.java b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/DmDialect.java new file mode 100644 index 000000000..888a2c7a2 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/DmDialect.java @@ -0,0 +1,104 @@ +package com.alibaba.smart.framework.engine.dialect.impl; + +import com.alibaba.smart.framework.engine.dialect.IdGenerationType; + +/** + * 达梦 (DM) database dialect implementation. + * DM is a Chinese domestic database with high Oracle compatibility. + * + * @author SmartEngine Team + */ +public class DmDialect extends AbstractDialect { + + @Override + public String getName() { + return "dm"; + } + + @Override + public String getDisplayName() { + return "达梦数据库 (DM)"; + } + + @Override + public String buildPageSql(String sql, int offset, int limit) { + // DM supports LIMIT OFFSET syntax + return sql + " LIMIT " + limit + " OFFSET " + offset; + } + + @Override + public boolean supportsLimitOffset() { + return true; + } + + @Override + public String getCurrentTimestamp() { + return "CURRENT_TIMESTAMP"; + } + + @Override + public String getDateDiff(String startColumn, String endColumn) { + return "DATEDIFF(DAY, " + startColumn + ", " + endColumn + ")"; + } + + @Override + public String getVarcharType(int length) { + return "VARCHAR2(" + length + ")"; + } + + @Override + public String getClobType() { + return "CLOB"; + } + + @Override + public String getTimestampType() { + return "TIMESTAMP"; + } + + @Override + public IdGenerationType getIdGenerationType() { + return IdGenerationType.IDENTITY; + } + + @Override + public String getAutoIncrementDefinition() { + return "IDENTITY"; + } + + @Override + public String getSequenceNextValueSql(String sequenceName) { + return sequenceName + ".NEXTVAL"; + } + + @Override + public String concat(String... expressions) { + // DM supports || for concatenation (Oracle compatible) + return "(" + String.join(" || ", expressions) + ")"; + } + + @Override + public String substring(String column, int start, int length) { + return "SUBSTR(" + column + ", " + start + ", " + length + ")"; + } + + @Override + public boolean supportsFeature(DialectFeature feature) { + switch (feature) { + case LIMIT_OFFSET: + case SEQUENCES: + case WINDOW_FUNCTIONS: + case COMMON_TABLE_EXPRESSIONS: + return true; + case LATERAL_JOINS: + case JSON_FUNCTIONS: + return true; + case RETURNING_CLAUSE: + case ARRAY_TYPE: + case UPSERT: + return true; + default: + return false; + } + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/H2Dialect.java b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/H2Dialect.java new file mode 100644 index 000000000..aae13677e --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/H2Dialect.java @@ -0,0 +1,85 @@ +package com.alibaba.smart.framework.engine.dialect.impl; + +import com.alibaba.smart.framework.engine.dialect.IdGenerationType; + +/** + * H2 database dialect implementation. + * + * @author SmartEngine Team + */ +public class H2Dialect extends AbstractDialect { + + @Override + public String getName() { + return "h2"; + } + + @Override + public String getDisplayName() { + return "H2 Database"; + } + + @Override + public String buildPageSql(String sql, int offset, int limit) { + return sql + " LIMIT " + limit + " OFFSET " + offset; + } + + @Override + public boolean supportsLimitOffset() { + return true; + } + + @Override + public String getCurrentTimestamp() { + return "CURRENT_TIMESTAMP"; + } + + @Override + public String getDateDiff(String startColumn, String endColumn) { + return "DATEDIFF('DAY', " + startColumn + ", " + endColumn + ")"; + } + + @Override + public String getClobType() { + return "CLOB"; + } + + @Override + public IdGenerationType getIdGenerationType() { + return IdGenerationType.AUTO_INCREMENT; + } + + @Override + public String getAutoIncrementDefinition() { + return "AUTO_INCREMENT"; + } + + @Override + public String getSequenceNextValueSql(String sequenceName) { + return "NEXT VALUE FOR " + sequenceName; + } + + @Override + public String jsonExtractText(String column, String key) { + return "JSON_VALUE(" + column + " FORMAT JSON, '$." + key + "')"; + } + + @Override + public boolean supportsFeature(DialectFeature feature) { + switch (feature) { + case LIMIT_OFFSET: + case SEQUENCES: + case WINDOW_FUNCTIONS: + case COMMON_TABLE_EXPRESSIONS: + return true; + case LATERAL_JOINS: + case RETURNING_CLAUSE: + case JSON_FUNCTIONS: + case ARRAY_TYPE: + case UPSERT: + return true; // H2 supports MERGE + default: + return false; + } + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/KingbaseDialect.java b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/KingbaseDialect.java new file mode 100644 index 000000000..45d434936 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/KingbaseDialect.java @@ -0,0 +1,96 @@ +package com.alibaba.smart.framework.engine.dialect.impl; + +import com.alibaba.smart.framework.engine.dialect.IdGenerationType; + +/** + * 人大金仓 (KingbaseES) database dialect implementation. + * KingbaseES is a Chinese domestic database with high PostgreSQL compatibility. + * + * @author SmartEngine Team + */ +public class KingbaseDialect extends AbstractDialect { + + @Override + public String getName() { + return "kingbase"; + } + + @Override + public String getDisplayName() { + return "人大金仓 (KingbaseES)"; + } + + @Override + public String buildPageSql(String sql, int offset, int limit) { + // KingbaseES supports LIMIT OFFSET (PostgreSQL compatible) + return sql + " LIMIT " + limit + " OFFSET " + offset; + } + + @Override + public boolean supportsLimitOffset() { + return true; + } + + @Override + public String getCurrentTimestamp() { + return "CURRENT_TIMESTAMP"; + } + + @Override + public String getDateDiff(String startColumn, String endColumn) { + // PostgreSQL compatible + return "(" + endColumn + " - " + startColumn + ")"; + } + + @Override + public String getClobType() { + return "TEXT"; + } + + @Override + public IdGenerationType getIdGenerationType() { + return IdGenerationType.IDENTITY; + } + + @Override + public String getAutoIncrementDefinition() { + return "GENERATED ALWAYS AS IDENTITY"; + } + + @Override + public String getSequenceNextValueSql(String sequenceName) { + return "nextval('" + sequenceName + "')"; + } + + @Override + public String concat(String... expressions) { + // PostgreSQL compatible: use || operator + return "(" + String.join(" || ", expressions) + ")"; + } + + @Override + public String jsonExtractText(String column, String key) { + // PostgreSQL compatible + return "(" + column + "->>'" + key + "')"; + } + + @Override + public boolean supportsFeature(DialectFeature feature) { + switch (feature) { + case LIMIT_OFFSET: + case SEQUENCES: + case WINDOW_FUNCTIONS: + case COMMON_TABLE_EXPRESSIONS: + case LATERAL_JOINS: + return true; + case RETURNING_CLAUSE: + case JSON_FUNCTIONS: + case ARRAY_TYPE: + return true; + case UPSERT: + return true; // ON CONFLICT + default: + return false; + } + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/MySqlDialect.java b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/MySqlDialect.java new file mode 100644 index 000000000..a771d15ea --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/MySqlDialect.java @@ -0,0 +1,86 @@ +package com.alibaba.smart.framework.engine.dialect.impl; + +import com.alibaba.smart.framework.engine.dialect.IdGenerationType; + +/** + * MySQL dialect implementation. + * + * @author SmartEngine Team + */ +public class MySqlDialect extends AbstractDialect { + + @Override + public String getName() { + return "mysql"; + } + + @Override + public String getDisplayName() { + return "MySQL"; + } + + @Override + public String buildPageSql(String sql, int offset, int limit) { + return sql + " LIMIT " + limit + " OFFSET " + offset; + } + + @Override + public boolean supportsLimitOffset() { + return true; + } + + @Override + public String getCurrentTimestamp() { + return "CURRENT_TIMESTAMP"; + } + + @Override + public String getDateDiff(String startColumn, String endColumn) { + return "DATEDIFF(" + endColumn + ", " + startColumn + ")"; + } + + @Override + public String getClobType() { + return "TEXT"; + } + + @Override + public IdGenerationType getIdGenerationType() { + return IdGenerationType.AUTO_INCREMENT; + } + + @Override + public String getAutoIncrementDefinition() { + return "AUTO_INCREMENT"; + } + + @Override + public String quoteIdentifier(String identifier) { + return "`" + identifier + "`"; + } + + @Override + public String jsonExtractText(String column, String key) { + return "JSON_UNQUOTE(JSON_EXTRACT(" + column + ", '$." + key + "'))"; + } + + @Override + public boolean supportsFeature(DialectFeature feature) { + switch (feature) { + case LIMIT_OFFSET: + case WINDOW_FUNCTIONS: + case COMMON_TABLE_EXPRESSIONS: + case JSON_FUNCTIONS: + return true; + case SEQUENCES: + case LATERAL_JOINS: + case RETURNING_CLAUSE: + case ARRAY_TYPE: + return false; + case UPSERT: + return true; // ON DUPLICATE KEY UPDATE + default: + return false; + } + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/OceanBaseDialect.java b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/OceanBaseDialect.java new file mode 100644 index 000000000..4a5e95949 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/OceanBaseDialect.java @@ -0,0 +1,90 @@ +package com.alibaba.smart.framework.engine.dialect.impl; + +import com.alibaba.smart.framework.engine.dialect.IdGenerationType; + +/** + * OceanBase database dialect implementation. + * OceanBase is a distributed database with MySQL compatibility mode. + * + * @author SmartEngine Team + */ +public class OceanBaseDialect extends AbstractDialect { + + @Override + public String getName() { + return "oceanbase"; + } + + @Override + public String getDisplayName() { + return "OceanBase"; + } + + @Override + public String buildPageSql(String sql, int offset, int limit) { + // OceanBase MySQL mode supports LIMIT OFFSET + return sql + " LIMIT " + limit + " OFFSET " + offset; + } + + @Override + public boolean supportsLimitOffset() { + return true; + } + + @Override + public String getCurrentTimestamp() { + return "CURRENT_TIMESTAMP"; + } + + @Override + public String getDateDiff(String startColumn, String endColumn) { + return "DATEDIFF(" + endColumn + ", " + startColumn + ")"; + } + + @Override + public String getClobType() { + return "LONGTEXT"; + } + + @Override + public IdGenerationType getIdGenerationType() { + return IdGenerationType.AUTO_INCREMENT; + } + + @Override + public String getAutoIncrementDefinition() { + return "AUTO_INCREMENT"; + } + + @Override + public String quoteIdentifier(String identifier) { + return "`" + identifier + "`"; + } + + @Override + public String jsonExtractText(String column, String key) { + // MySQL compatible + return "JSON_UNQUOTE(JSON_EXTRACT(" + column + ", '$." + key + "'))"; + } + + @Override + public boolean supportsFeature(DialectFeature feature) { + switch (feature) { + case LIMIT_OFFSET: + case WINDOW_FUNCTIONS: + case COMMON_TABLE_EXPRESSIONS: + return true; + case SEQUENCES: + return false; // MySQL mode doesn't support sequences + case LATERAL_JOINS: + case RETURNING_CLAUSE: + case ARRAY_TYPE: + return false; + case JSON_FUNCTIONS: + case UPSERT: + return true; + default: + return false; + } + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/OracleDialect.java b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/OracleDialect.java new file mode 100644 index 000000000..0d5732337 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/OracleDialect.java @@ -0,0 +1,113 @@ +package com.alibaba.smart.framework.engine.dialect.impl; + +import com.alibaba.smart.framework.engine.dialect.IdGenerationType; + +/** + * Oracle database dialect implementation. + * + * @author SmartEngine Team + */ +public class OracleDialect extends AbstractDialect { + + @Override + public String getName() { + return "oracle"; + } + + @Override + public String getDisplayName() { + return "Oracle Database"; + } + + @Override + public String buildPageSql(String sql, int offset, int limit) { + // Oracle 12c+ supports OFFSET FETCH + return sql + " OFFSET " + offset + " ROWS FETCH NEXT " + limit + " ROWS ONLY"; + } + + /** + * Build page SQL for older Oracle versions (before 12c). + * + * @param sql the original SQL + * @param offset the offset + * @param limit the limit + * @return the paginated SQL using ROWNUM + */ + public String buildPageSqlLegacy(String sql, int offset, int limit) { + int endRow = offset + limit; + return "SELECT * FROM (SELECT t.*, ROWNUM rn FROM (" + sql + ") t WHERE ROWNUM <= " + endRow + + ") WHERE rn > " + offset; + } + + @Override + public boolean supportsLimitOffset() { + return true; // Oracle 12c+ + } + + @Override + public String getCurrentTimestamp() { + return "SYSTIMESTAMP"; + } + + @Override + public String getDateDiff(String startColumn, String endColumn) { + return "(" + endColumn + " - " + startColumn + ")"; + } + + @Override + public String getVarcharType(int length) { + return "VARCHAR2(" + length + ")"; + } + + @Override + public String getClobType() { + return "CLOB"; + } + + @Override + public String getTimestampType() { + return "TIMESTAMP(6)"; + } + + @Override + public IdGenerationType getIdGenerationType() { + return IdGenerationType.SEQUENCE; + } + + @Override + public String getSequenceNextValueSql(String sequenceName) { + return sequenceName + ".NEXTVAL"; + } + + @Override + public String concat(String... expressions) { + // Oracle uses || for concatenation + return "(" + String.join(" || ", expressions) + ")"; + } + + @Override + public String substring(String column, int start, int length) { + return "SUBSTR(" + column + ", " + start + ", " + length + ")"; + } + + @Override + public boolean supportsFeature(DialectFeature feature) { + switch (feature) { + case SEQUENCES: + case WINDOW_FUNCTIONS: + case COMMON_TABLE_EXPRESSIONS: + case LATERAL_JOINS: + return true; + case LIMIT_OFFSET: + return true; // Oracle 12c+ + case RETURNING_CLAUSE: + case JSON_FUNCTIONS: + return true; + case ARRAY_TYPE: + case UPSERT: + return true; // MERGE statement + default: + return false; + } + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/PostgreSqlDialect.java b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/PostgreSqlDialect.java new file mode 100644 index 000000000..e8464bfdd --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/PostgreSqlDialect.java @@ -0,0 +1,92 @@ +package com.alibaba.smart.framework.engine.dialect.impl; + +import com.alibaba.smart.framework.engine.dialect.IdGenerationType; + +/** + * PostgreSQL dialect implementation. + * + * @author SmartEngine Team + */ +public class PostgreSqlDialect extends AbstractDialect { + + @Override + public String getName() { + return "postgresql"; + } + + @Override + public String getDisplayName() { + return "PostgreSQL"; + } + + @Override + public String buildPageSql(String sql, int offset, int limit) { + return sql + " LIMIT " + limit + " OFFSET " + offset; + } + + @Override + public boolean supportsLimitOffset() { + return true; + } + + @Override + public String getCurrentTimestamp() { + return "CURRENT_TIMESTAMP"; + } + + @Override + public String getDateDiff(String startColumn, String endColumn) { + return "(" + endColumn + " - " + startColumn + ")"; + } + + @Override + public String getClobType() { + return "TEXT"; + } + + @Override + public IdGenerationType getIdGenerationType() { + return IdGenerationType.IDENTITY; + } + + @Override + public String getAutoIncrementDefinition() { + // PostgreSQL uses SERIAL or GENERATED ... AS IDENTITY + return "GENERATED ALWAYS AS IDENTITY"; + } + + @Override + public String getSequenceNextValueSql(String sequenceName) { + return "nextval('" + sequenceName + "')"; + } + + @Override + public String concat(String... expressions) { + // PostgreSQL supports || operator for concatenation + return "(" + String.join(" || ", expressions) + ")"; + } + + @Override + public String jsonExtractText(String column, String key) { + return "(" + column + "->>'" + key + "')"; + } + + @Override + public boolean supportsFeature(DialectFeature feature) { + switch (feature) { + case LIMIT_OFFSET: + case SEQUENCES: + case WINDOW_FUNCTIONS: + case COMMON_TABLE_EXPRESSIONS: + case LATERAL_JOINS: + case RETURNING_CLAUSE: + case JSON_FUNCTIONS: + case ARRAY_TYPE: + return true; + case UPSERT: + return true; // ON CONFLICT DO UPDATE + default: + return false; + } + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/SqlServerDialect.java b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/SqlServerDialect.java new file mode 100644 index 000000000..f12529d7d --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/dialect/impl/SqlServerDialect.java @@ -0,0 +1,105 @@ +package com.alibaba.smart.framework.engine.dialect.impl; + +import com.alibaba.smart.framework.engine.dialect.IdGenerationType; + +/** + * Microsoft SQL Server dialect implementation. + * + * @author SmartEngine Team + */ +public class SqlServerDialect extends AbstractDialect { + + @Override + public String getName() { + return "sqlserver"; + } + + @Override + public String getDisplayName() { + return "Microsoft SQL Server"; + } + + @Override + public String buildPageSql(String sql, int offset, int limit) { + // SQL Server 2012+ supports OFFSET FETCH + // Requires ORDER BY clause + return sql + " OFFSET " + offset + " ROWS FETCH NEXT " + limit + " ROWS ONLY"; + } + + @Override + public boolean supportsLimitOffset() { + return true; // SQL Server 2012+ + } + + @Override + public String getCurrentTimestamp() { + return "GETDATE()"; + } + + @Override + public String getDateDiff(String startColumn, String endColumn) { + return "DATEDIFF(DAY, " + startColumn + ", " + endColumn + ")"; + } + + @Override + public String getVarcharType(int length) { + return "NVARCHAR(" + length + ")"; + } + + @Override + public String getClobType() { + return "NVARCHAR(MAX)"; + } + + @Override + public String getTimestampType() { + return "DATETIME2"; + } + + @Override + public IdGenerationType getIdGenerationType() { + return IdGenerationType.IDENTITY; + } + + @Override + public String getAutoIncrementDefinition() { + return "IDENTITY(1,1)"; + } + + @Override + public String concat(String... expressions) { + return "CONCAT(" + String.join(", ", expressions) + ")"; + } + + @Override + public String quoteIdentifier(String identifier) { + return "[" + identifier + "]"; + } + + @Override + public String getForUpdateNoWaitClause() { + return "WITH (UPDLOCK, NOWAIT)"; + } + + @Override + public boolean supportsFeature(DialectFeature feature) { + switch (feature) { + case WINDOW_FUNCTIONS: + case COMMON_TABLE_EXPRESSIONS: + case JSON_FUNCTIONS: + return true; + case LIMIT_OFFSET: + return true; // SQL Server 2012+ + case SEQUENCES: + return true; // SQL Server 2012+ + case LATERAL_JOINS: + case RETURNING_CLAUSE: + case ARRAY_TYPE: + return false; + case UPSERT: + return true; // MERGE statement + default: + return false; + } + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/exception/NotificationException.java b/core/src/main/java/com/alibaba/smart/framework/engine/exception/NotificationException.java new file mode 100644 index 000000000..b2dd94e07 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/exception/NotificationException.java @@ -0,0 +1,21 @@ +package com.alibaba.smart.framework.engine.exception; + +/** + * 知会通知相关异常 + * + * @author SmartEngine Team + */ +public class NotificationException extends EngineException { + + private static final long serialVersionUID = 1L; + + public NotificationException(String message) { + super(message); + } + + public NotificationException(String message, Throwable cause) { + super(message, cause); + } + + +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/exception/RollbackException.java b/core/src/main/java/com/alibaba/smart/framework/engine/exception/RollbackException.java new file mode 100644 index 000000000..40d44dbda --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/exception/RollbackException.java @@ -0,0 +1,27 @@ +package com.alibaba.smart.framework.engine.exception; + +/** + * 流程回退相关异常 + * + * @author SmartEngine Team + */ +public class RollbackException extends EngineException { + + private static final long serialVersionUID = -2066455487370828007L; + + public RollbackException(String message) { + super(message); + } + + public RollbackException(Exception e) { + super(e); + } + + public RollbackException(String message, Exception e) { + super(message, e); + } + + public RollbackException(String message, Throwable e) { + super(message, e); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/exception/ShardingKeyRequiredException.java b/core/src/main/java/com/alibaba/smart/framework/engine/exception/ShardingKeyRequiredException.java new file mode 100644 index 000000000..b108e2a42 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/exception/ShardingKeyRequiredException.java @@ -0,0 +1,14 @@ +package com.alibaba.smart.framework.engine.exception; + +/** + * Thrown when a query that requires a sharding key (processInstanceId) is invoked + * without providing one in sharding mode. + */ +public class ShardingKeyRequiredException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public ShardingKeyRequiredException(String message) { + super(message); + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/exception/SupervisionException.java b/core/src/main/java/com/alibaba/smart/framework/engine/exception/SupervisionException.java new file mode 100644 index 000000000..5008003b9 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/exception/SupervisionException.java @@ -0,0 +1,21 @@ +package com.alibaba.smart.framework.engine.exception; + +/** + * 督办相关异常 + * + * @author SmartEngine Team + */ +public class SupervisionException extends EngineException { + + private static final long serialVersionUID = 1L; + + public SupervisionException(String message) { + super(message); + } + + public SupervisionException(String message, Throwable cause) { + super(message, cause); + } + + +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/extension/constant/ExtensionConstant.java b/core/src/main/java/com/alibaba/smart/framework/engine/extension/constant/ExtensionConstant.java index e09e89541..111b16a1d 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/extension/constant/ExtensionConstant.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/extension/constant/ExtensionConstant.java @@ -6,6 +6,8 @@ public interface ExtensionConstant { String COMMON = "common"; + String CUSTOM = "custom"; + String SERVICE = "SERVICE"; String ELEMENT_PARSER = "element-parser"; diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/extension/scanner/SimpleAnnotationScanner.java b/core/src/main/java/com/alibaba/smart/framework/engine/extension/scanner/SimpleAnnotationScanner.java index bd180d8dd..257b4093d 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/extension/scanner/SimpleAnnotationScanner.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/extension/scanner/SimpleAnnotationScanner.java @@ -4,6 +4,8 @@ import java.io.FileFilter; import java.io.IOException; import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; import java.net.JarURLConnection; import java.net.URL; import java.net.URLDecoder; @@ -21,9 +23,12 @@ import com.alibaba.smart.framework.engine.configuration.scanner.ExtensionBindingResult; import com.alibaba.smart.framework.engine.exception.EngineException; import com.alibaba.smart.framework.engine.extension.annotation.ExtensionBinding; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.storage.StorageRouter; import com.alibaba.smart.framework.engine.util.ClassUtil; import lombok.Getter; +import lombok.Setter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,6 +44,9 @@ public class SimpleAnnotationScanner implements AnnotationScanner { @Getter private Map scanResult = new HashMap(); + @Setter + private StorageRouter storageRouter; + private String[] packageNameList; public SimpleAnnotationScanner(String... packageNameList) {this.packageNameList = packageNameList;} @@ -249,11 +257,37 @@ else if(currentBindingAnnotation.priority() == exBindingAnnotation.priority()){ @Override public T getExtensionPoint(String group, Class clazz) { + // For storage types registered in StorageRouter, return a dynamic proxy + // that routes based on current StorageMode at invocation time. + // This is critical because service classes cache the storage reference during init(), + // so a direct reference would be locked to a single mode. + if (storageRouter != null && ExtensionConstant.COMMON.equals(group) + && storageRouter.hasStorageType(clazz)) { + return createStorageProxy(clazz); + } + + // Default behavior (unchanged) ExtensionBindingResult extensionBindingResult = this.scanResult.get(group); Map bindingMap = extensionBindingResult.getBindingMap(); return (T)bindingMap.get(clazz); } + @SuppressWarnings("unchecked") + private T createStorageProxy(Class storageInterface) { + return (T) Proxy.newProxyInstance( + storageInterface.getClassLoader(), + new Class[]{storageInterface}, + (proxy, method, args) -> { + T realStorage = storageRouter.getStorage(storageInterface); + try { + return method.invoke(realStorage, args); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + ); + } + @Override public Object getObject(String group, Class clazz) { return this.scanResult.get(group).getBindingMap().get(clazz); diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultAssigneeOperationRecord.java b/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultAssigneeOperationRecord.java new file mode 100644 index 000000000..2d490a81a --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultAssigneeOperationRecord.java @@ -0,0 +1,32 @@ +package com.alibaba.smart.framework.engine.instance.impl; + +import com.alibaba.smart.framework.engine.model.instance.AssigneeOperationRecord; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 默认加签减签操作记录实现 + * + * @author SmartEngine Team + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DefaultAssigneeOperationRecord extends AbstractLifeCycleInstance implements AssigneeOperationRecord { + + private static final long serialVersionUID = 1L; + + private String processInstanceId; + + private String taskInstanceId; + + private String operationType; + + private String operatorUserId; + + private String targetUserId; + + private String operationReason; +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultNotificationInstance.java b/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultNotificationInstance.java new file mode 100644 index 000000000..27e7f1784 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultNotificationInstance.java @@ -0,0 +1,40 @@ +package com.alibaba.smart.framework.engine.instance.impl; + +import java.util.Date; + +import com.alibaba.smart.framework.engine.model.instance.NotificationInstance; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 默认知会通知实例实现 + * + * @author SmartEngine Team + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DefaultNotificationInstance extends AbstractLifeCycleInstance implements NotificationInstance { + + private static final long serialVersionUID = 1L; + + private String processInstanceId; + + private String taskInstanceId; + + private String senderUserId; + + private String receiverUserId; + + private String notificationType; + + private String title; + + private String content; + + private String readStatus; + + private Date readTime; +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultRollbackRecord.java b/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultRollbackRecord.java new file mode 100644 index 000000000..b30fd3d94 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultRollbackRecord.java @@ -0,0 +1,34 @@ +package com.alibaba.smart.framework.engine.instance.impl; + +import com.alibaba.smart.framework.engine.model.instance.RollbackRecord; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 默认流程回退记录实现 + * + * @author SmartEngine Team + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DefaultRollbackRecord extends AbstractLifeCycleInstance implements RollbackRecord { + + private static final long serialVersionUID = 1L; + + private String processInstanceId; + + private String taskInstanceId; + + private String rollbackType; + + private String fromActivityId; + + private String toActivityId; + + private String operatorUserId; + + private String rollbackReason; +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultSupervisionInstance.java b/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultSupervisionInstance.java new file mode 100644 index 000000000..e2b855007 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultSupervisionInstance.java @@ -0,0 +1,36 @@ +package com.alibaba.smart.framework.engine.instance.impl; + +import java.util.Date; + +import com.alibaba.smart.framework.engine.model.instance.SupervisionInstance; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 默认督办实例实现 + * + * @author SmartEngine Team + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DefaultSupervisionInstance extends AbstractLifeCycleInstance implements SupervisionInstance { + + private static final long serialVersionUID = 1L; + + private String processInstanceId; + + private String taskInstanceId; + + private String supervisorUserId; + + private String supervisionReason; + + private String supervisionType; + + private String status; + + private Date closeTime; +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultTaskInstance.java b/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultTaskInstance.java index c7c447c44..32f2ac88c 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultTaskInstance.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultTaskInstance.java @@ -53,6 +53,10 @@ public class DefaultTaskInstance extends AbstractLifeCycleInstance implements Ta */ private String extension; + private String domainCode; + + private String extra; + /** * 任务标题 */ diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultTaskTransferRecord.java b/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultTaskTransferRecord.java new file mode 100644 index 000000000..ba07cd1e8 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/instance/impl/DefaultTaskTransferRecord.java @@ -0,0 +1,34 @@ +package com.alibaba.smart.framework.engine.instance.impl; + +import java.util.Date; + +import com.alibaba.smart.framework.engine.model.instance.TaskTransferRecord; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 默认任务移交记录实现 + * + * @author SmartEngine Team + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DefaultTaskTransferRecord extends AbstractLifeCycleInstance implements TaskTransferRecord { + + private static final long serialVersionUID = 1L; + + private String processInstanceId; + + private String taskInstanceId; + + private String fromUserId; + + private String toUserId; + + private String transferReason; + + private Date deadline; +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/instance/storage/AssigneeOperationRecordStorage.java b/core/src/main/java/com/alibaba/smart/framework/engine/instance/storage/AssigneeOperationRecordStorage.java new file mode 100644 index 000000000..e83f21999 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/instance/storage/AssigneeOperationRecordStorage.java @@ -0,0 +1,44 @@ +package com.alibaba.smart.framework.engine.instance.storage; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.model.instance.AssigneeOperationRecord; + +import java.util.List; + +/** + * 加签减签操作记录存储接口 + * + * @author SmartEngine Team + */ +public interface AssigneeOperationRecordStorage { + + /** + * 插入加签减签操作记录 + */ + AssigneeOperationRecord insert(AssigneeOperationRecord assigneeOperationRecord, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 根据任务ID查询操作记录列表 + */ + List findByTaskId(Long taskInstanceId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 根据ID查询操作记录 + */ + AssigneeOperationRecord find(Long id, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 更新操作记录 + */ + AssigneeOperationRecord update(AssigneeOperationRecord assigneeOperationRecord, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 删除操作记录 + */ + void remove(Long id, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/instance/storage/NotificationInstanceStorage.java b/core/src/main/java/com/alibaba/smart/framework/engine/instance/storage/NotificationInstanceStorage.java new file mode 100644 index 000000000..9cf63e9ee --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/instance/storage/NotificationInstanceStorage.java @@ -0,0 +1,83 @@ +package com.alibaba.smart.framework.engine.instance.storage; + +import java.util.List; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.model.instance.NotificationInstance; +import com.alibaba.smart.framework.engine.service.param.query.NotificationQueryParam; + +/** + * 知会通知实例存储接口 + * + * @author SmartEngine Team + */ +public interface NotificationInstanceStorage { + + /** + * 插入知会通知实例 + */ + NotificationInstance insert(NotificationInstance notificationInstance, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 更新知会通知实例 + */ + NotificationInstance update(NotificationInstance notificationInstance, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 根据ID查询知会通知实例 + */ + NotificationInstance find(String notificationId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 查询知会通知实例列表 + */ + List findNotificationList(NotificationQueryParam param, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 统计知会通知实例数量 + */ + Long countNotifications(NotificationQueryParam param, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 统计未读通知数量 + */ + Long countUnreadNotifications(String receiverUserId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 查询接收人的知会列表 + */ + List findByReceiver(String receiverUserId, String readStatus, + String tenantId, Integer pageOffset, Integer pageSize, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 查询发送人的知会列表 + */ + List findBySender(String senderUserId, String tenantId, + Integer pageOffset, Integer pageSize, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 标记为已读 + */ + int markAsRead(String notificationId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 批量标记为已读 + */ + int batchMarkAsRead(List notificationIds, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 删除知会通知实例 + */ + void remove(String notificationId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/instance/storage/RollbackRecordStorage.java b/core/src/main/java/com/alibaba/smart/framework/engine/instance/storage/RollbackRecordStorage.java new file mode 100644 index 000000000..3bcc330e1 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/instance/storage/RollbackRecordStorage.java @@ -0,0 +1,44 @@ +package com.alibaba.smart.framework.engine.instance.storage; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.model.instance.RollbackRecord; + +import java.util.List; + +/** + * 流程回退记录存储接口 + * + * @author SmartEngine Team + */ +public interface RollbackRecordStorage { + + /** + * 插入回退记录 + */ + RollbackRecord insert(RollbackRecord rollbackRecord, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 根据流程实例ID查询回退记录列表 + */ + List findByProcessInstanceId(Long processInstanceId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 根据ID查询回退记录 + */ + RollbackRecord find(Long id, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 更新回退记录 + */ + RollbackRecord update(RollbackRecord rollbackRecord, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 删除回退记录 + */ + void remove(Long id, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/instance/storage/SupervisionInstanceStorage.java b/core/src/main/java/com/alibaba/smart/framework/engine/instance/storage/SupervisionInstanceStorage.java new file mode 100644 index 000000000..10e75397e --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/instance/storage/SupervisionInstanceStorage.java @@ -0,0 +1,69 @@ +package com.alibaba.smart.framework.engine.instance.storage; + +import java.util.List; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.model.instance.SupervisionInstance; +import com.alibaba.smart.framework.engine.service.param.query.SupervisionQueryParam; + +/** + * 督办实例存储接口 + * + * @author SmartEngine Team + */ +public interface SupervisionInstanceStorage { + + /** + * 插入督办实例 + */ + SupervisionInstance insert(SupervisionInstance supervisionInstance, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 更新督办实例 + */ + SupervisionInstance update(SupervisionInstance supervisionInstance, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 根据ID查询督办实例 + */ + SupervisionInstance find(String supervisionId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 查询督办实例列表 + */ + List findSupervisionList(SupervisionQueryParam param, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 统计督办实例数量 + */ + Long countSupervision(SupervisionQueryParam param, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 查询任务的活跃督办记录 + */ + List findActiveSupervisionByTask(String taskInstanceId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 关闭督办记录 + */ + int closeSupervision(String supervisionId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 根据任务ID批量关闭督办记录 + */ + int closeSupervisionByTask(String taskInstanceId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 删除督办实例 + */ + void remove(String supervisionId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/instance/storage/TaskTransferRecordStorage.java b/core/src/main/java/com/alibaba/smart/framework/engine/instance/storage/TaskTransferRecordStorage.java new file mode 100644 index 000000000..433df6c72 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/instance/storage/TaskTransferRecordStorage.java @@ -0,0 +1,44 @@ +package com.alibaba.smart.framework.engine.instance.storage; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.model.instance.TaskTransferRecord; + +import java.util.List; + +/** + * 任务移交记录存储接口 + * + * @author SmartEngine Team + */ +public interface TaskTransferRecordStorage { + + /** + * 插入任务移交记录 + */ + TaskTransferRecord insert(TaskTransferRecord taskTransferRecord, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 根据任务ID查询移交记录列表 + */ + List findByTaskId(Long taskInstanceId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 根据ID查询移交记录 + */ + TaskTransferRecord find(Long id, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 更新移交记录 + */ + TaskTransferRecord update(TaskTransferRecord taskTransferRecord, + ProcessEngineConfiguration processEngineConfiguration); + + /** + * 删除移交记录 + */ + void remove(Long id, String tenantId, + ProcessEngineConfiguration processEngineConfiguration); +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/AssigneeOperationRecord.java b/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/AssigneeOperationRecord.java new file mode 100644 index 000000000..829d99be2 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/AssigneeOperationRecord.java @@ -0,0 +1,69 @@ +package com.alibaba.smart.framework.engine.model.instance; + +/** + * 加签减签操作记录接口 + * + * @author SmartEngine Team + */ +public interface AssigneeOperationRecord extends LifeCycleInstance { + + /** + * 获取流程实例ID + */ + String getProcessInstanceId(); + + /** + * 设置流程实例ID + */ + void setProcessInstanceId(String processInstanceId); + + /** + * 获取任务实例ID + */ + String getTaskInstanceId(); + + /** + * 设置任务实例ID + */ + void setTaskInstanceId(String taskInstanceId); + + /** + * 获取操作类型(add_assignee/remove_assignee) + */ + String getOperationType(); + + /** + * 设置操作类型 + */ + void setOperationType(String operationType); + + /** + * 获取操作人用户ID + */ + String getOperatorUserId(); + + /** + * 设置操作人用户ID + */ + void setOperatorUserId(String operatorUserId); + + /** + * 获取目标用户ID + */ + String getTargetUserId(); + + /** + * 设置目标用户ID + */ + void setTargetUserId(String targetUserId); + + /** + * 获取操作原因 + */ + String getOperationReason(); + + /** + * 设置操作原因 + */ + void setOperationReason(String operationReason); +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/NotificationInstance.java b/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/NotificationInstance.java new file mode 100644 index 000000000..3c29b27cd --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/NotificationInstance.java @@ -0,0 +1,101 @@ +package com.alibaba.smart.framework.engine.model.instance; + +import java.util.Date; + +/** + * 知会通知实例接口 + * + * @author SmartEngine Team + */ +public interface NotificationInstance extends LifeCycleInstance { + + /** + * 获取流程实例ID + */ + String getProcessInstanceId(); + + /** + * 设置流程实例ID + */ + void setProcessInstanceId(String processInstanceId); + + /** + * 获取任务实例ID + */ + String getTaskInstanceId(); + + /** + * 设置任务实例ID + */ + void setTaskInstanceId(String taskInstanceId); + + /** + * 获取发送人用户ID + */ + String getSenderUserId(); + + /** + * 设置发送人用户ID + */ + void setSenderUserId(String senderUserId); + + /** + * 获取接收人用户ID + */ + String getReceiverUserId(); + + /** + * 设置接收人用户ID + */ + void setReceiverUserId(String receiverUserId); + + /** + * 获取通知类型 + */ + String getNotificationType(); + + /** + * 设置通知类型 + */ + void setNotificationType(String notificationType); + + /** + * 获取通知标题 + */ + String getTitle(); + + /** + * 设置通知标题 + */ + void setTitle(String title); + + /** + * 获取通知内容 + */ + String getContent(); + + /** + * 设置通知内容 + */ + void setContent(String content); + + /** + * 获取读取状态 + */ + String getReadStatus(); + + /** + * 设置读取状态 + */ + void setReadStatus(String readStatus); + + /** + * 获取读取时间 + */ + Date getReadTime(); + + /** + * 设置读取时间 + */ + void setReadTime(Date readTime); +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/RollbackRecord.java b/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/RollbackRecord.java new file mode 100644 index 000000000..e22748f59 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/RollbackRecord.java @@ -0,0 +1,79 @@ +package com.alibaba.smart.framework.engine.model.instance; + +/** + * 流程回退记录接口 + * + * @author SmartEngine Team + */ +public interface RollbackRecord extends LifeCycleInstance { + + /** + * 获取流程实例ID + */ + String getProcessInstanceId(); + + /** + * 设置流程实例ID + */ + void setProcessInstanceId(String processInstanceId); + + /** + * 获取任务实例ID + */ + String getTaskInstanceId(); + + /** + * 设置任务实例ID + */ + void setTaskInstanceId(String taskInstanceId); + + /** + * 获取回退类型(specific/previous) + */ + String getRollbackType(); + + /** + * 设置回退类型 + */ + void setRollbackType(String rollbackType); + + /** + * 获取源活动ID + */ + String getFromActivityId(); + + /** + * 设置源活动ID + */ + void setFromActivityId(String fromActivityId); + + /** + * 获取目标活动ID + */ + String getToActivityId(); + + /** + * 设置目标活动ID + */ + void setToActivityId(String toActivityId); + + /** + * 获取操作人用户ID + */ + String getOperatorUserId(); + + /** + * 设置操作人用户ID + */ + void setOperatorUserId(String operatorUserId); + + /** + * 获取回退原因 + */ + String getRollbackReason(); + + /** + * 设置回退原因 + */ + void setRollbackReason(String rollbackReason); +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/SupervisionInstance.java b/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/SupervisionInstance.java new file mode 100644 index 000000000..134411637 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/SupervisionInstance.java @@ -0,0 +1,81 @@ +package com.alibaba.smart.framework.engine.model.instance; + +import java.util.Date; + +/** + * 督办实例接口 + * + * @author SmartEngine Team + */ +public interface SupervisionInstance extends LifeCycleInstance { + + /** + * 获取流程实例ID + */ + String getProcessInstanceId(); + + /** + * 设置流程实例ID + */ + void setProcessInstanceId(String processInstanceId); + + /** + * 获取任务实例ID + */ + String getTaskInstanceId(); + + /** + * 设置任务实例ID + */ + void setTaskInstanceId(String taskInstanceId); + + /** + * 获取督办人用户ID + */ + String getSupervisorUserId(); + + /** + * 设置督办人用户ID + */ + void setSupervisorUserId(String supervisorUserId); + + /** + * 获取督办原因 + */ + String getSupervisionReason(); + + /** + * 设置督办原因 + */ + void setSupervisionReason(String supervisionReason); + + /** + * 获取督办类型 + */ + String getSupervisionType(); + + /** + * 设置督办类型 + */ + void setSupervisionType(String supervisionType); + + /** + * 获取督办状态 + */ + String getStatus(); + + /** + * 设置督办状态 + */ + void setStatus(String status); + + /** + * 获取关闭时间 + */ + Date getCloseTime(); + + /** + * 设置关闭时间 + */ + void setCloseTime(Date closeTime); +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/TaskInstance.java b/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/TaskInstance.java index ad21b987c..030fbeb87 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/TaskInstance.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/TaskInstance.java @@ -74,6 +74,13 @@ public interface TaskInstance extends LifeCycleInstance { void setExtension(String extension); + String getDomainCode(); + + void setDomainCode(String domainCode); + + String getExtra(); + + void setExtra(String extra); String getTitle(); diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/TaskTransferRecord.java b/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/TaskTransferRecord.java new file mode 100644 index 000000000..ece457ec8 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/model/instance/TaskTransferRecord.java @@ -0,0 +1,71 @@ +package com.alibaba.smart.framework.engine.model.instance; + +import java.util.Date; + +/** + * 任务移交记录接口 + * + * @author SmartEngine Team + */ +public interface TaskTransferRecord extends LifeCycleInstance { + + /** + * 获取流程实例ID + */ + String getProcessInstanceId(); + + /** + * 设置流程实例ID + */ + void setProcessInstanceId(String processInstanceId); + + /** + * 获取任务实例ID + */ + String getTaskInstanceId(); + + /** + * 设置任务实例ID + */ + void setTaskInstanceId(String taskInstanceId); + + /** + * 获取移交人用户ID + */ + String getFromUserId(); + + /** + * 设置移交人用户ID + */ + void setFromUserId(String fromUserId); + + /** + * 获取接收人用户ID + */ + String getToUserId(); + + /** + * 设置接收人用户ID + */ + void setToUserId(String toUserId); + + /** + * 获取移交原因 + */ + String getTransferReason(); + + /** + * 设置移交原因 + */ + void setTransferReason(String transferReason); + + /** + * 获取截止时间 + */ + Date getDeadline(); + + /** + * 设置截止时间 + */ + void setDeadline(Date deadline); +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/pvm/event/EventConstant.java b/core/src/main/java/com/alibaba/smart/framework/engine/pvm/event/EventConstant.java index f233c56d3..d271ff585 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/pvm/event/EventConstant.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/pvm/event/EventConstant.java @@ -17,8 +17,14 @@ public enum EventConstant { ACTIVITY_EXECUTE, ACTIVITY_END, - - + // Task lifecycle events (fired via TaskEventPublisher SPI) + TASK_ASSIGNED, // Task created and assigned to candidate(s) + TASK_CLAIMED, // Task claimed by a specific user + TASK_COMPLETED, // Task completed (approved/processed) + TASK_CANCELED, // Task canceled (e.g. countersign decision made by others) + TASK_TRANSFERRED, // Task transferred to another user + TASK_DELEGATED, // New assignee added to task + TASK_REVOKED, // Assignee removed from task diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/query/DeploymentQuery.java b/core/src/main/java/com/alibaba/smart/framework/engine/query/DeploymentQuery.java new file mode 100644 index 000000000..65808fddd --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/query/DeploymentQuery.java @@ -0,0 +1,169 @@ +package com.alibaba.smart.framework.engine.query; + +import com.alibaba.smart.framework.engine.model.instance.DeploymentInstance; + +/** + * Fluent query interface for DeploymentInstance. + * Provides type-safe method chaining for building deployment queries. + * + *

Example usage: + *

{@code
+ * List deployments = smartEngine.createDeploymentQuery()
+ *     .processDefinitionType("approval")
+ *     .deploymentStatus(DeploymentStatusConstant.ACTIVE)
+ *     .orderByModifyTime().desc()
+ *     .listPage(0, 10);
+ * }
+ * + * @author SmartEngine Team + */ +public interface DeploymentQuery extends Query { + + // ============ Filter conditions ============ + + /** + * Filter by deployment instance ID. + * + * @param deploymentInstanceId the deployment instance ID + * @return this query for method chaining + */ + DeploymentQuery deploymentInstanceId(String deploymentInstanceId); + + /** + * Filter by process definition version. + * + * @param version the process definition version + * @return this query for method chaining + */ + DeploymentQuery processDefinitionVersion(String version); + + /** + * Filter by process definition type. + * + * @param processDefinitionType the process definition type + * @return this query for method chaining + */ + DeploymentQuery processDefinitionType(String processDefinitionType); + + /** + * Conditionally filter by process definition type. + * + * @param condition if true, the filter is applied + * @param processDefinitionType the process definition type + * @return this query for method chaining + */ + DeploymentQuery processDefinitionType(boolean condition, String processDefinitionType); + + /** + * Filter by process definition code (key). + * + * @param processDefinitionCode the process definition code + * @return this query for method chaining + */ + DeploymentQuery processDefinitionCode(String processDefinitionCode); + + /** + * Filter by process definition name (exact match). + * + * @param processDefinitionName the process definition name + * @return this query for method chaining + */ + DeploymentQuery processDefinitionName(String processDefinitionName); + + /** + * Filter by process definition name (fuzzy match, LIKE %nameLike%). + * + * @param processDefinitionNameLike the name pattern + * @return this query for method chaining + */ + DeploymentQuery processDefinitionNameLike(String processDefinitionNameLike); + + /** + * Conditionally filter by process definition name (fuzzy match). + * + * @param condition if true, the filter is applied + * @param processDefinitionNameLike the name pattern + * @return this query for method chaining + */ + DeploymentQuery processDefinitionNameLike(boolean condition, String processDefinitionNameLike); + + /** + * Filter by process definition description (fuzzy match, LIKE %descLike%). + * + * @param processDefinitionDescLike the description pattern + * @return this query for method chaining + */ + DeploymentQuery processDefinitionDescLike(String processDefinitionDescLike); + + /** + * Filter by deployment user ID. + * + * @param deploymentUserId the user who deployed + * @return this query for method chaining + */ + DeploymentQuery deploymentUserId(String deploymentUserId); + + /** + * Filter by deployment status. + * + * @param deploymentStatus the deployment status + * @return this query for method chaining + * @see com.alibaba.smart.framework.engine.constant.DeploymentStatusConstant + */ + DeploymentQuery deploymentStatus(String deploymentStatus); + + /** + * Conditionally filter by deployment status. + * + * @param condition if true, the filter is applied + * @param deploymentStatus the deployment status + * @return this query for method chaining + */ + DeploymentQuery deploymentStatus(boolean condition, String deploymentStatus); + + /** + * Filter by logic status. + * + * @param logicStatus the logic status + * @return this query for method chaining + * @see com.alibaba.smart.framework.engine.constant.LogicStatusConstant + */ + DeploymentQuery logicStatus(String logicStatus); + + // ============ Ordering ============ + + /** + * Order by deployment instance ID. + * + * @return this query for method chaining (call asc() or desc() next) + */ + DeploymentQuery orderByDeploymentId(); + + /** + * Order by create time. + * + * @return this query for method chaining (call asc() or desc() next) + */ + DeploymentQuery orderByCreateTime(); + + /** + * Order by modify time. + * + * @return this query for method chaining (call asc() or desc() next) + */ + DeploymentQuery orderByModifyTime(); + + /** + * Set ascending order for the previous orderBy call. + * + * @return this query for method chaining + */ + DeploymentQuery asc(); + + /** + * Set descending order for the previous orderBy call. + * + * @return this query for method chaining + */ + DeploymentQuery desc(); +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/query/NotificationQuery.java b/core/src/main/java/com/alibaba/smart/framework/engine/query/NotificationQuery.java new file mode 100644 index 000000000..3c2d2e535 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/query/NotificationQuery.java @@ -0,0 +1,141 @@ +package com.alibaba.smart.framework.engine.query; + +import java.util.Date; +import java.util.List; + +import com.alibaba.smart.framework.engine.model.instance.NotificationInstance; + +/** + * Fluent query interface for NotificationInstance. + * Provides type-safe method chaining for building notification queries. + * + *

Example usage: + *

{@code
+ * List notifications = smartEngine.createNotificationQuery()
+ *     .receiverUserId("user001")
+ *     .readStatus("unread")
+ *     .orderByCreateTime().desc()
+ *     .listPage(0, 10);
+ * }
+ * + * @author SmartEngine Team + */ +public interface NotificationQuery extends ProcessBoundQuery { + + // ============ Filter conditions ============ + + /** + * Filter by notification instance ID. + * + * @param notificationId the notification instance ID + * @return this query for method chaining + */ + NotificationQuery notificationId(String notificationId); + + /** + * Filter by sender user ID. + * + * @param senderUserId the sender user ID + * @return this query for method chaining + */ + NotificationQuery senderUserId(String senderUserId); + + /** + * Filter by receiver user ID. + * + * @param receiverUserId the receiver user ID + * @return this query for method chaining + */ + NotificationQuery receiverUserId(String receiverUserId); + + /** + * Conditionally filter by receiver user ID. + * + * @param condition if true, the filter is applied + * @param receiverUserId the receiver user ID + * @return this query for method chaining + */ + NotificationQuery receiverUserId(boolean condition, String receiverUserId); + + /** + * Filter by task instance ID. + * + * @param taskInstanceId the task instance ID + * @return this query for method chaining + */ + NotificationQuery taskInstanceId(String taskInstanceId); + + /** + * Filter by multiple task instance IDs. + * + * @param taskInstanceIds the list of task instance IDs + * @return this query for method chaining + */ + NotificationQuery taskInstanceIdIn(List taskInstanceIds); + + /** + * Filter by notification type. + * + * @param notificationType the notification type + * @return this query for method chaining + */ + NotificationQuery notificationType(String notificationType); + + /** + * Conditionally filter by notification type. + * + * @param condition if true, the filter is applied + * @param notificationType the notification type + * @return this query for method chaining + */ + NotificationQuery notificationType(boolean condition, String notificationType); + + /** + * Filter by read status. + * + * @param readStatus the read status + * @return this query for method chaining + */ + NotificationQuery readStatus(String readStatus); + + /** + * Conditionally filter by read status. + * + * @param condition if true, the filter is applied + * @param readStatus the read status + * @return this query for method chaining + */ + NotificationQuery readStatus(boolean condition, String readStatus); + + /** + * Filter by notification time range (start). + * + * @param startTime the start of notification time range + * @return this query for method chaining + */ + NotificationQuery notificationTimeAfter(Date startTime); + + /** + * Filter by notification time range (end). + * + * @param endTime the end of notification time range + * @return this query for method chaining + */ + NotificationQuery notificationTimeBefore(Date endTime); + + // ============ Ordering ============ + + /** + * Order by notification instance ID. + * + * @return this query for method chaining (call asc() or desc() next) + */ + NotificationQuery orderByNotificationId(); + + /** + * Order by read time. + * + * @return this query for method chaining (call asc() or desc() next) + */ + NotificationQuery orderByReadTime(); +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/query/OrderSpec.java b/core/src/main/java/com/alibaba/smart/framework/engine/query/OrderSpec.java new file mode 100644 index 000000000..81ee7ae68 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/query/OrderSpec.java @@ -0,0 +1,86 @@ +package com.alibaba.smart.framework.engine.query; + +import java.io.Serializable; + +/** + * Order specification for dynamic sorting in queries. + * Used to build ORDER BY clauses dynamically. + * + * @author SmartEngine Team + */ +public class OrderSpec implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Column name to sort by (database column name) + */ + private final String columnName; + + /** + * Sort direction: ASC or DESC + */ + private final Direction direction; + + /** + * Property name (Java field name for reference) + */ + private final String propertyName; + + public enum Direction { + ASC("ASC"), + DESC("DESC"); + + private final String sql; + + Direction(String sql) { + this.sql = sql; + } + + public String getSql() { + return sql; + } + } + + public OrderSpec(String propertyName, String columnName, Direction direction) { + this.propertyName = propertyName; + this.columnName = columnName; + this.direction = direction; + } + + public String getColumnName() { + return columnName; + } + + public String getPropertyName() { + return propertyName; + } + + public Direction getDirection() { + return direction; + } + + /** + * Get the column name for SQL ORDER BY clause. + * This method is used in MyBatis XML. + * + * @return the column name + */ + public String toColumnName() { + return columnName; + } + + /** + * Get the SQL direction string. + * + * @return "ASC" or "DESC" + */ + public String getDirectionSql() { + return direction.getSql(); + } + + @Override + public String toString() { + return columnName + " " + direction.getSql(); + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/query/ProcessBoundQuery.java b/core/src/main/java/com/alibaba/smart/framework/engine/query/ProcessBoundQuery.java new file mode 100644 index 000000000..ea91e907f --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/query/ProcessBoundQuery.java @@ -0,0 +1,67 @@ +package com.alibaba.smart.framework.engine.query; + +import java.util.List; + +/** + * Shared query interface for entities bound to a process instance. + * Provides common filtering by processInstanceId and shared ordering. + * Inspired by Flowable's TaskInfoQuery shared interface pattern. + * + * @param the query type itself for method chaining + * @param the result entity type + */ +public interface ProcessBoundQuery, T> extends Query { + + /** + * Filter by process instance ID. + * + * @param processInstanceId the process instance ID + * @return this query for method chaining + */ + Q processInstanceId(String processInstanceId); + + /** + * Conditionally filter by process instance ID. + * + * @param condition if true, the filter is applied + * @param processInstanceId the process instance ID + * @return this query for method chaining + */ + Q processInstanceId(boolean condition, String processInstanceId); + + /** + * Filter by multiple process instance IDs. + * + * @param processInstanceIds the list of process instance IDs + * @return this query for method chaining + */ + Q processInstanceIdIn(List processInstanceIds); + + /** + * Order by create time. + * + * @return this query for method chaining (call asc() or desc() next) + */ + Q orderByCreateTime(); + + /** + * Order by modify time. + * + * @return this query for method chaining (call asc() or desc() next) + */ + Q orderByModifyTime(); + + /** + * Set ascending order for the previous orderBy call. + * + * @return this query for method chaining + */ + Q asc(); + + /** + * Set descending order for the previous orderBy call. + * + * @return this query for method chaining + */ + Q desc(); +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/query/ProcessInstanceQuery.java b/core/src/main/java/com/alibaba/smart/framework/engine/query/ProcessInstanceQuery.java new file mode 100644 index 000000000..52f649f02 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/query/ProcessInstanceQuery.java @@ -0,0 +1,240 @@ +package com.alibaba.smart.framework.engine.query; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import com.alibaba.smart.framework.engine.model.instance.ProcessInstance; + +/** + * Fluent query interface for ProcessInstance. + * Provides type-safe method chaining for building process instance queries. + * + *

Example usage: + *

{@code
+ * List processes = smartEngine.createProcessQuery()
+ *     .processDefinitionType("approval")
+ *     .startedBy("user001")
+ *     .processStatus(InstanceStatus.running)
+ *     .orderByStartTime().desc()
+ *     .listPage(0, 10);
+ * }
+ * + * @author SmartEngine Team + */ +public interface ProcessInstanceQuery extends Query { + + // ============ Filter conditions ============ + + /** + * Filter by process instance ID. + * + * @param processInstanceId the process instance ID + * @return this query for method chaining + */ + ProcessInstanceQuery processInstanceId(String processInstanceId); + + /** + * Conditionally filter by process instance ID. + * + * @param condition if true, the filter is applied + * @param processInstanceId the process instance ID + * @return this query for method chaining + */ + ProcessInstanceQuery processInstanceId(boolean condition, String processInstanceId); + + /** + * Filter by multiple process instance IDs. + * + * @param processInstanceIds the list of process instance IDs + * @return this query for method chaining + */ + ProcessInstanceQuery processInstanceIdIn(List processInstanceIds); + + /** + * Filter by start user ID. + * + * @param startUserId the user ID who started the process + * @return this query for method chaining + */ + ProcessInstanceQuery startedBy(String startUserId); + + /** + * Conditionally filter by start user ID. + * + * @param condition if true, the filter is applied + * @param startUserId the user ID who started the process + * @return this query for method chaining + */ + ProcessInstanceQuery startedBy(boolean condition, String startUserId); + + /** + * Filter by process status. + * + * @param status the process status (use InstanceStatus enum value name) + * @return this query for method chaining + */ + ProcessInstanceQuery processStatus(String status); + + /** + * Conditionally filter by process status. + * + * @param condition if true, the filter is applied + * @param status the process status + * @return this query for method chaining + */ + ProcessInstanceQuery processStatus(boolean condition, String status); + + /** + * Filter by process definition type. + * + * @param processDefinitionType the process definition type + * @return this query for method chaining + */ + ProcessInstanceQuery processDefinitionType(String processDefinitionType); + + /** + * Conditionally filter by process definition type. + * + * @param condition if true, the filter is applied + * @param processDefinitionType the process definition type + * @return this query for method chaining + */ + ProcessInstanceQuery processDefinitionType(boolean condition, String processDefinitionType); + + /** + * Filter by process definition ID and version. + * + * @param processDefinitionIdAndVersion the process definition ID and version + * @return this query for method chaining + */ + ProcessInstanceQuery processDefinitionIdAndVersion(String processDefinitionIdAndVersion); + + /** + * Filter by parent instance ID. + * + * @param parentInstanceId the parent process instance ID + * @return this query for method chaining + */ + ProcessInstanceQuery parentInstanceId(String parentInstanceId); + + /** + * Filter by business unique ID. + * + * @param bizUniqueId the business unique ID + * @return this query for method chaining + */ + ProcessInstanceQuery bizUniqueId(String bizUniqueId); + + /** + * Conditionally filter by business unique ID. + * + * @param condition if true, the filter is applied + * @param bizUniqueId the business unique ID + * @return this query for method chaining + */ + ProcessInstanceQuery bizUniqueId(boolean condition, String bizUniqueId); + + /** + * Filter by start time range (start). + * + * @param startTimeAfter the start of time range + * @return this query for method chaining + */ + ProcessInstanceQuery startedAfter(Date startTimeAfter); + + /** + * Filter by start time range (end). + * + * @param startTimeBefore the end of time range + * @return this query for method chaining + */ + ProcessInstanceQuery startedBefore(Date startTimeBefore); + + /** + * Filter by complete time range (start). + * + * @param completeTimeStart the start of complete time range + * @return this query for method chaining + */ + ProcessInstanceQuery completedAfter(Date completeTimeStart); + + /** + * Filter by complete time range (end). + * + * @param completeTimeEnd the end of complete time range + * @return this query for method chaining + */ + ProcessInstanceQuery completedBefore(Date completeTimeEnd); + + /** + * Filter by multiple process definition types. + * + * @param types the list of process definition types + * @return this query for method chaining + */ + ProcessInstanceQuery processDefinitionTypeIn(List types); + + /** + * Filter by multiple process definition types (varargs). + * + * @param types the process definition types + * @return this query for method chaining + */ + default ProcessInstanceQuery processDefinitionTypeIn(String... types) { + return processDefinitionTypeIn(Arrays.asList(types)); + } + + /** + * Filter by involved user. Finds processes where the user is the starter. + * This is a semantic alias for {@link #startedBy(String)}. + * + * @param userId the involved user ID + * @return this query for method chaining + */ + ProcessInstanceQuery involvedUser(String userId); + + // ============ Ordering ============ + + /** + * Order by process instance ID. + * + * @return this query for method chaining (call asc() or desc() next) + */ + ProcessInstanceQuery orderByProcessInstanceId(); + + /** + * Order by start time (create time). + * + * @return this query for method chaining (call asc() or desc() next) + */ + ProcessInstanceQuery orderByStartTime(); + + /** + * Order by modify time. + * + * @return this query for method chaining (call asc() or desc() next) + */ + ProcessInstanceQuery orderByModifyTime(); + + /** + * Order by complete time. + * + * @return this query for method chaining (call asc() or desc() next) + */ + ProcessInstanceQuery orderByCompleteTime(); + + /** + * Set ascending order for the previous orderBy call. + * + * @return this query for method chaining + */ + ProcessInstanceQuery asc(); + + /** + * Set descending order for the previous orderBy call. + * + * @return this query for method chaining + */ + ProcessInstanceQuery desc(); +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/query/Query.java b/core/src/main/java/com/alibaba/smart/framework/engine/query/Query.java new file mode 100644 index 000000000..f2f8ba7c9 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/query/Query.java @@ -0,0 +1,109 @@ +package com.alibaba.smart.framework.engine.query; + +import java.util.List; + +/** + * Base interface for fluent query API. + * Provides common query operations like pagination and sorting. + * + * @param the query type itself for method chaining + * @param the result entity type + * @author SmartEngine Team + */ +public interface Query, T> { + + // ============ Pagination ============ + + /** + * Set the page offset (0-based). + * + * @param offset page offset, starting from 0 + * @return this query for method chaining + */ + Q pageOffset(int offset); + + /** + * Set the page size. + * + * @param size maximum number of results to return + * @return this query for method chaining + */ + Q pageSize(int size); + + // ============ Tenant ============ + + /** + * Filter by tenant ID. + * + * @param tenantId the tenant ID + * @return this query for method chaining + */ + Q tenantId(String tenantId); + + /** + * Conditionally filter by tenant ID. + * + * @param condition if true, the tenantId filter is applied + * @param tenantId the tenant ID + * @return this query for method chaining + */ + Q tenantId(boolean condition, String tenantId); + + /** + * Conditionally set the page offset (0-based). + * + * @param condition if true, the offset is applied + * @param offset page offset, starting from 0 + * @return this query for method chaining + */ + Q pageOffset(boolean condition, int offset); + + /** + * Conditionally set the page size. + * + * @param condition if true, the size is applied + * @param size maximum number of results to return + * @return this query for method chaining + */ + Q pageSize(boolean condition, int size); + + /** + * Exclude tenant filtering. Useful for cross-tenant queries. + * + * @return this query for method chaining + */ + Q withoutTenantId(); + + // ============ Execution methods ============ + + /** + * Execute the query and return all matching results. + * + * @return list of matching entities + */ + List list(); + + /** + * Execute the query with pagination and return matching results. + * + * @param offset page offset (0-based) + * @param limit maximum number of results + * @return list of matching entities + */ + List listPage(int offset, int limit); + + /** + * Execute the query and return the single result. + * + * @return the single result, or null if not found + * @throws IllegalStateException if more than one result is found + */ + T singleResult(); + + /** + * Execute the query and return the count of matching entities. + * + * @return the count + */ + long count(); +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/query/SupervisionQuery.java b/core/src/main/java/com/alibaba/smart/framework/engine/query/SupervisionQuery.java new file mode 100644 index 000000000..5ea3498b3 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/query/SupervisionQuery.java @@ -0,0 +1,133 @@ +package com.alibaba.smart.framework.engine.query; + +import java.util.Date; +import java.util.List; + +import com.alibaba.smart.framework.engine.model.instance.SupervisionInstance; + +/** + * Fluent query interface for SupervisionInstance. + * Provides type-safe method chaining for building supervision queries. + * + *

Example usage: + *

{@code
+ * List supervisions = smartEngine.createSupervisionQuery()
+ *     .supervisorUserId("supervisor001")
+ *     .supervisionStatus("active")
+ *     .orderByCreateTime().desc()
+ *     .listPage(0, 10);
+ * }
+ * + * @author SmartEngine Team + */ +public interface SupervisionQuery extends ProcessBoundQuery { + + // ============ Filter conditions ============ + + /** + * Filter by supervision instance ID. + * + * @param supervisionId the supervision instance ID + * @return this query for method chaining + */ + SupervisionQuery supervisionId(String supervisionId); + + /** + * Filter by supervisor user ID. + * + * @param supervisorUserId the supervisor user ID + * @return this query for method chaining + */ + SupervisionQuery supervisorUserId(String supervisorUserId); + + /** + * Conditionally filter by supervisor user ID. + * + * @param condition if true, the filter is applied + * @param supervisorUserId the supervisor user ID + * @return this query for method chaining + */ + SupervisionQuery supervisorUserId(boolean condition, String supervisorUserId); + + /** + * Filter by task instance ID. + * + * @param taskInstanceId the task instance ID + * @return this query for method chaining + */ + SupervisionQuery taskInstanceId(String taskInstanceId); + + /** + * Filter by multiple task instance IDs. + * + * @param taskInstanceIds the list of task instance IDs + * @return this query for method chaining + */ + SupervisionQuery taskInstanceIdIn(List taskInstanceIds); + + /** + * Filter by supervision type. + * + * @param supervisionType the supervision type + * @return this query for method chaining + */ + SupervisionQuery supervisionType(String supervisionType); + + /** + * Conditionally filter by supervision type. + * + * @param condition if true, the filter is applied + * @param supervisionType the supervision type + * @return this query for method chaining + */ + SupervisionQuery supervisionType(boolean condition, String supervisionType); + + /** + * Filter by supervision status. + * + * @param status the supervision status + * @return this query for method chaining + */ + SupervisionQuery supervisionStatus(String status); + + /** + * Conditionally filter by supervision status. + * + * @param condition if true, the filter is applied + * @param status the supervision status + * @return this query for method chaining + */ + SupervisionQuery supervisionStatus(boolean condition, String status); + + /** + * Filter by supervision time range (start). + * + * @param startTime the start of supervision time range + * @return this query for method chaining + */ + SupervisionQuery supervisionTimeAfter(Date startTime); + + /** + * Filter by supervision time range (end). + * + * @param endTime the end of supervision time range + * @return this query for method chaining + */ + SupervisionQuery supervisionTimeBefore(Date endTime); + + // ============ Ordering ============ + + /** + * Order by supervision instance ID. + * + * @return this query for method chaining (call asc() or desc() next) + */ + SupervisionQuery orderBySupervisionId(); + + /** + * Order by close time. + * + * @return this query for method chaining (call asc() or desc() next) + */ + SupervisionQuery orderByCloseTime(); +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/query/TaskQuery.java b/core/src/main/java/com/alibaba/smart/framework/engine/query/TaskQuery.java new file mode 100644 index 000000000..f3cbc6c48 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/query/TaskQuery.java @@ -0,0 +1,450 @@ +package com.alibaba.smart.framework.engine.query; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import com.alibaba.smart.framework.engine.model.instance.TaskInstance; + +/** + * Fluent query interface for TaskInstance. + * Provides type-safe method chaining for building task queries. + * + *

Example usage: + *

{@code
+ * List tasks = smartEngine.createTaskQuery()
+ *     .processInstanceId("12345")
+ *     .taskAssignee("user001")
+ *     .taskStatus(TaskInstanceConstant.PENDING)
+ *     .orderByCreateTime().desc()
+ *     .listPage(0, 10);
+ * }
+ * + * @author SmartEngine Team + */ +public interface TaskQuery extends ProcessBoundQuery { + + // ============ Filter conditions ============ + + /** + * Filter by task instance ID. + * + * @param taskInstanceId the task instance ID + * @return this query for method chaining + */ + TaskQuery taskInstanceId(String taskInstanceId); + + /** + * Filter by activity instance ID. + * + * @param activityInstanceId the activity instance ID + * @return this query for method chaining + */ + TaskQuery activityInstanceId(String activityInstanceId); + + /** + * Filter by process definition type. + * + * @param processDefinitionType the process definition type + * @return this query for method chaining + */ + TaskQuery processDefinitionType(String processDefinitionType); + + /** + * Conditionally filter by process definition type. + * + * @param condition if true, the filter is applied + * @param processDefinitionType the process definition type + * @return this query for method chaining + */ + TaskQuery processDefinitionType(boolean condition, String processDefinitionType); + + /** + * Filter by process definition activity ID. + * + * @param processDefinitionActivityId the process definition activity ID + * @return this query for method chaining + */ + TaskQuery processDefinitionActivityId(String processDefinitionActivityId); + + /** + * Filter by task status. + * + * @param status the task status + * @return this query for method chaining + * @see com.alibaba.smart.framework.engine.constant.TaskInstanceConstant + */ + TaskQuery taskStatus(String status); + + /** + * Conditionally filter by task status. + * + * @param condition if true, the filter is applied + * @param status the task status + * @return this query for method chaining + */ + TaskQuery taskStatus(boolean condition, String status); + + /** + * Filter by multiple task statuses. + * + * @param statuses the list of task statuses + * @return this query for method chaining + */ + TaskQuery taskStatusIn(List statuses); + + /** + * Filter by multiple task statuses (varargs). + * + * @param statuses the task statuses + * @return this query for method chaining + */ + TaskQuery taskStatusIn(String... statuses); + + /** + * Filter by claim user ID (task assignee). + * + * @param claimUserId the user ID who claimed the task + * @return this query for method chaining + */ + TaskQuery taskAssignee(String claimUserId); + + /** + * Conditionally filter by claim user ID (task assignee). + * + * @param condition if true, the filter is applied + * @param claimUserId the user ID who claimed the task + * @return this query for method chaining + */ + TaskQuery taskAssignee(boolean condition, String claimUserId); + + /** + * Filter by candidate user ID (via assignee table JOIN). + * Finds tasks where the specified user is a candidate assignee (assignee_type = 'user'). + * + *

This differs from {@link #taskAssignee(String)} which filters by the already-claimed user. + * This method queries the task assignee candidate table to find tasks assigned to the user. + * + * @param userId the candidate user ID + * @return this query for method chaining + */ + TaskQuery taskCandidateUser(String userId); + + /** + * Filter by candidate group ID (via assignee table JOIN). + * Finds tasks where the specified group is a candidate assignee (assignee_type = 'group'). + * + * @param groupId the candidate group ID + * @return this query for method chaining + */ + TaskQuery taskCandidateGroup(String groupId); + + /** + * Filter by multiple candidate group IDs (via assignee table JOIN). + * Finds tasks where any of the specified groups is a candidate assignee. + * + * @param groupIds the candidate group ID list + * @return this query for method chaining + */ + TaskQuery taskCandidateGroupIn(List groupIds); + + /** + * Filter by multiple candidate group IDs (varargs). + * + * @param groupIds the candidate group IDs + * @return this query for method chaining + */ + default TaskQuery taskCandidateGroupIn(String... groupIds) { + return taskCandidateGroupIn(Arrays.asList(groupIds)); + } + + /** + * Filter by candidate user OR candidate groups (via assignee table JOIN). + * Finds tasks where the user is a candidate (assignee_type = 'user') + * OR any of the groups is a candidate (assignee_type = 'group'). + * + *

This is the most common pattern for pending task queries: + *

{@code
+     * List myTasks = smartEngine.createTaskQuery()
+     *     .taskCandidateOrGroup("user001", Arrays.asList("dept-A", "role-manager"))
+     *     .taskStatus(TaskInstanceConstant.PENDING)
+     *     .listPage(0, 10);
+     * }
+ * + * @param userId the candidate user ID + * @param groupIds the candidate group ID list + * @return this query for method chaining + */ + TaskQuery taskCandidateOrGroup(String userId, List groupIds); + + /** + * Filter by tag. + * + * @param tag the task tag + * @return this query for method chaining + */ + TaskQuery taskTag(String tag); + + /** + * Conditionally filter by tag. + * + * @param condition if true, the filter is applied + * @param tag the task tag + * @return this query for method chaining + */ + TaskQuery taskTag(boolean condition, String tag); + + /** + * Filter by extension field. + * + * @param extension the extension value + * @return this query for method chaining + */ + TaskQuery taskExtension(String extension); + + /** + * Filter by priority. + * + * @param priority the task priority + * @return this query for method chaining + */ + TaskQuery taskPriority(Integer priority); + + /** + * Filter by comment. + * + * @param comment the task comment + * @return this query for method chaining + */ + TaskQuery taskComment(String comment); + + /** + * Filter by title. + * + * @param title the task title + * @return this query for method chaining + */ + TaskQuery taskTitle(String title); + + /** + * Conditionally filter by title. + * + * @param condition if true, the filter is applied + * @param title the task title + * @return this query for method chaining + */ + TaskQuery taskTitle(boolean condition, String title); + + /** + * Filter by complete time range (start). + * + * @param completeTimeStart the start of complete time range + * @return this query for method chaining + */ + TaskQuery completeTimeAfter(Date completeTimeStart); + + /** + * Filter by complete time range (end). + * + * @param completeTimeEnd the end of complete time range + * @return this query for method chaining + */ + TaskQuery completeTimeBefore(Date completeTimeEnd); + + /** + * Filter by create time range (start, inclusive). + * + * @param createTimeStart tasks created at or after this time + * @return this query for method chaining + */ + TaskQuery createdAfter(Date createTimeStart); + + /** + * Filter by create time range (end, exclusive). + * + * @param createTimeEnd tasks created before this time + * @return this query for method chaining + */ + TaskQuery createdBefore(Date createTimeEnd); + + /** + * Filter by title using fuzzy match (LIKE %titleLike%). + * + * @param titleLike the title pattern to match + * @return this query for method chaining + */ + TaskQuery taskTitleLike(String titleLike); + + /** + * Conditionally filter by title using fuzzy match. + * + * @param condition if true, the filter is applied + * @param titleLike the title pattern to match + * @return this query for method chaining + */ + TaskQuery taskTitleLike(boolean condition, String titleLike); + + /** + * Filter for unassigned tasks (no claim user). + * + * @return this query for method chaining + */ + TaskQuery taskUnassigned(); + + /** + * Filter by multiple process definition types. + * + * @param types the list of process definition types + * @return this query for method chaining + */ + TaskQuery processDefinitionTypeIn(List types); + + /** + * Filter by multiple process definition types (varargs). + * + * @param types the process definition types + * @return this query for method chaining + */ + default TaskQuery processDefinitionTypeIn(String... types) { + return processDefinitionTypeIn(Arrays.asList(types)); + } + + /** + * Filter by assignee using fuzzy match (LIKE %claimUserIdLike%). + * + * @param claimUserIdLike the assignee pattern to match + * @return this query for method chaining + */ + TaskQuery taskAssigneeLike(String claimUserIdLike); + + /** + * Filter by minimum priority (inclusive). + * + * @param minPriority the minimum priority value + * @return this query for method chaining + */ + TaskQuery taskMinPriority(Integer minPriority); + + /** + * Filter by maximum priority (inclusive). + * + * @param maxPriority the maximum priority value + * @return this query for method chaining + */ + TaskQuery taskMaxPriority(Integer maxPriority); + + // ============ Domain code filters ============ + + /** + * Filter by domain code (exact match). + * + * @param domainCode the domain code + * @return this query for method chaining + */ + TaskQuery domainCode(String domainCode); + + /** + * Conditionally filter by domain code. + * + * @param condition if true, the filter is applied + * @param domainCode the domain code + * @return this query for method chaining + */ + TaskQuery domainCode(boolean condition, String domainCode); + + /** + * Filter by multiple domain codes (IN clause). + * + * @param domainCodes the list of domain codes + * @return this query for method chaining + */ + TaskQuery domainCodeIn(List domainCodes); + + /** + * Filter by multiple domain codes (varargs). + * + * @param domainCodes the domain codes + * @return this query for method chaining + */ + default TaskQuery domainCodeIn(String... domainCodes) { + return domainCodeIn(Arrays.asList(domainCodes)); + } + + /** + * Filter by domain code using fuzzy match (LIKE %domainCodeLike%). + * + * @param domainCodeLike the domain code pattern to match + * @return this query for method chaining + */ + TaskQuery domainCodeLike(String domainCodeLike); + + // ============ Extra JSON filters ============ + + /** + * Filter by a JSON key-value in the extra field (exact match). + * + * @param key JSON key (single-level, e.g., "category") + * @param value expected value + * @return this query for method chaining + */ + TaskQuery extraJson(String key, String value); + + /** + * Filter by JSON key matching any of the given values (IN clause). + * + * @param key JSON key (single-level) + * @param values the list of expected values + * @return this query for method chaining + */ + TaskQuery extraJsonIn(String key, List values); + + /** + * Filter by JSON key matching any of the given values (varargs). + * + * @param key JSON key (single-level) + * @param values the expected values + * @return this query for method chaining + */ + default TaskQuery extraJsonIn(String key, String... values) { + return extraJsonIn(key, Arrays.asList(values)); + } + + /** + * Filter by JSON key using LIKE match. + * + * @param key JSON key (single-level) + * @param pattern the LIKE pattern + * @return this query for method chaining + */ + TaskQuery extraJsonLike(String key, String pattern); + + // ============ Ordering ============ + + /** + * Order by task instance ID. + * + * @return this query for method chaining (call asc() or desc() next) + */ + TaskQuery orderByTaskId(); + + /** + * Order by claim time. + * + * @return this query for method chaining (call asc() or desc() next) + */ + TaskQuery orderByClaimTime(); + + /** + * Order by complete time. + * + * @return this query for method chaining (call asc() or desc() next) + */ + TaskQuery orderByCompleteTime(); + + /** + * Order by priority. + * + * @return this query for method chaining (call asc() or desc() next) + */ + TaskQuery orderByPriority(); +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/AbstractProcessBoundQuery.java b/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/AbstractProcessBoundQuery.java new file mode 100644 index 000000000..964f5fa21 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/AbstractProcessBoundQuery.java @@ -0,0 +1,90 @@ +package com.alibaba.smart.framework.engine.query.impl; + +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.query.ProcessBoundQuery; + +/** + * Abstract base for queries bound to process instances. + * Provides common processInstanceId filtering and ordering. + * + * @param the query type itself for method chaining + * @param the result entity type + */ +public abstract class AbstractProcessBoundQuery, T> + extends AbstractQuery implements ProcessBoundQuery { + + protected String processInstanceId; + protected List processInstanceIds; + + protected AbstractProcessBoundQuery(ProcessEngineConfiguration processEngineConfiguration) { + super(processEngineConfiguration); + } + + @Override + public Q processInstanceId(String processInstanceId) { + this.processInstanceId = processInstanceId; + return self(); + } + + @Override + public Q processInstanceId(boolean condition, String processInstanceId) { + if (condition) { + this.processInstanceId = processInstanceId; + } + return self(); + } + + @Override + public Q processInstanceIdIn(List processInstanceIds) { + this.processInstanceIds = processInstanceIds; + return self(); + } + + @Override + public Q orderByCreateTime() { + return orderBy("gmtCreate", "gmt_create"); + } + + @Override + public Q orderByModifyTime() { + return orderBy("gmtModified", "gmt_modified"); + } + + @Override + public Q asc() { + return applyAsc(); + } + + @Override + public Q desc() { + return applyDesc(); + } + + /** + * Build process instance ID list for query param. + * Helper for subclasses' buildQueryParam() methods. + * + * @return the process instance ID list, or null if not set + */ + protected List buildProcessInstanceIdList() { + if (processInstanceId != null) { + List ids = new ArrayList<>(); + ids.add(processInstanceId); + return ids; + } else if (processInstanceIds != null && !processInstanceIds.isEmpty()) { + return processInstanceIds; + } + return null; + } + + public String getProcessInstanceId() { + return processInstanceId; + } + + public List getProcessInstanceIds() { + return processInstanceIds; + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/AbstractQuery.java b/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/AbstractQuery.java new file mode 100644 index 000000000..dfd28ccab --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/AbstractQuery.java @@ -0,0 +1,206 @@ +package com.alibaba.smart.framework.engine.query.impl; + +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.query.OrderSpec; +import com.alibaba.smart.framework.engine.query.Query; + +/** + * Abstract base implementation for fluent query API. + * Provides common functionality for pagination, ordering, and execution. + * + * @param the query type itself for method chaining + * @param the result entity type + * @author SmartEngine Team + */ +public abstract class AbstractQuery, T> implements Query { + + protected ProcessEngineConfiguration processEngineConfiguration; + + protected Integer pageOffset; + protected Integer pageSize; + protected String tenantId; + protected boolean excludeTenant = false; + + protected List orderBySpecs = new ArrayList<>(); + protected String currentOrderByProperty; + protected String currentOrderByColumn; + + protected AbstractQuery(ProcessEngineConfiguration processEngineConfiguration) { + this.processEngineConfiguration = processEngineConfiguration; + } + + @SuppressWarnings("unchecked") + protected Q self() { + return (Q) this; + } + + // ============ Pagination ============ + + @Override + public Q pageOffset(int offset) { + this.pageOffset = offset; + return self(); + } + + @Override + public Q pageSize(int size) { + this.pageSize = size; + return self(); + } + + // ============ Tenant ============ + + @Override + public Q tenantId(String tenantId) { + this.tenantId = tenantId; + return self(); + } + + @Override + public Q tenantId(boolean condition, String tenantId) { + if (condition) { + this.tenantId = tenantId; + } + return self(); + } + + @Override + public Q pageOffset(boolean condition, int offset) { + if (condition) { + this.pageOffset = offset; + } + return self(); + } + + @Override + public Q pageSize(boolean condition, int size) { + if (condition) { + this.pageSize = size; + } + return self(); + } + + @Override + public Q withoutTenantId() { + this.excludeTenant = true; + return self(); + } + + // ============ Ordering helpers ============ + + /** + * Set up an order by with the given property and column names. + * The actual direction (asc/desc) will be set by a subsequent call. + * + * @param propertyName the Java property name + * @param columnName the database column name + * @return this query for method chaining + */ + protected Q orderBy(String propertyName, String columnName) { + this.currentOrderByProperty = propertyName; + this.currentOrderByColumn = columnName; + return self(); + } + + /** + * Apply ascending order to the current order by specification. + * + * @return this query for method chaining + */ + protected Q applyAsc() { + if (currentOrderByColumn != null) { + orderBySpecs.add(new OrderSpec(currentOrderByProperty, currentOrderByColumn, OrderSpec.Direction.ASC)); + currentOrderByProperty = null; + currentOrderByColumn = null; + } + return self(); + } + + /** + * Apply descending order to the current order by specification. + * + * @return this query for method chaining + */ + protected Q applyDesc() { + if (currentOrderByColumn != null) { + orderBySpecs.add(new OrderSpec(currentOrderByProperty, currentOrderByColumn, OrderSpec.Direction.DESC)); + currentOrderByProperty = null; + currentOrderByColumn = null; + } + return self(); + } + + // ============ Execution methods ============ + + @Override + public List list() { + List results = executeList(); + return results != null ? results : new ArrayList<>(); + } + + @Override + public List listPage(int offset, int limit) { + this.pageOffset = offset; + this.pageSize = limit; + List results = executeList(); + return results != null ? results : new ArrayList<>(); + } + + @Override + public T singleResult() { + List results = list(); + if (results.isEmpty()) { + return null; + } + if (results.size() > 1) { + throw new IllegalStateException("Query returned " + results.size() + " results instead of max 1"); + } + return results.get(0); + } + + @Override + public long count() { + return executeCount(); + } + + // ============ Abstract methods to be implemented by subclasses ============ + + /** + * Execute the list query. + * + * @return the list of results + */ + protected abstract List executeList(); + + /** + * Execute the count query. + * + * @return the count of results + */ + protected abstract long executeCount(); + + // ============ Getters for subclasses ============ + + public List getOrderBySpecs() { + return orderBySpecs; + } + + public Integer getPageOffset() { + return pageOffset; + } + + public Integer getPageSize() { + return pageSize; + } + + public String getTenantId() { + return tenantId; + } + + protected ProcessEngineConfiguration getProcessEngineConfiguration() { + return processEngineConfiguration; + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/DeploymentQueryImpl.java b/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/DeploymentQueryImpl.java new file mode 100644 index 000000000..ca60c6fc2 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/DeploymentQueryImpl.java @@ -0,0 +1,199 @@ +package com.alibaba.smart.framework.engine.query.impl; + +import java.util.List; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.instance.storage.DeploymentInstanceStorage; +import com.alibaba.smart.framework.engine.model.instance.DeploymentInstance; +import com.alibaba.smart.framework.engine.query.DeploymentQuery; +import com.alibaba.smart.framework.engine.service.param.query.DeploymentInstanceQueryParam; + +/** + * Implementation of DeploymentQuery fluent API. + * + * @author SmartEngine Team + */ +public class DeploymentQueryImpl extends AbstractQuery + implements DeploymentQuery { + + private DeploymentInstanceStorage deploymentInstanceStorage; + + // Filter conditions + private String deploymentInstanceId; + private String processDefinitionVersion; + private String processDefinitionType; + private String processDefinitionCode; + private String processDefinitionName; + private String processDefinitionNameLike; + private String processDefinitionDescLike; + private String deploymentUserId; + private String deploymentStatus; + private String logicStatus; + + public DeploymentQueryImpl(ProcessEngineConfiguration processEngineConfiguration) { + super(processEngineConfiguration); + this.deploymentInstanceStorage = processEngineConfiguration.getAnnotationScanner() + .getExtensionPoint(ExtensionConstant.COMMON, DeploymentInstanceStorage.class); + } + + // ============ Filter conditions ============ + + @Override + public DeploymentQuery deploymentInstanceId(String deploymentInstanceId) { + this.deploymentInstanceId = deploymentInstanceId; + return this; + } + + @Override + public DeploymentQuery processDefinitionVersion(String version) { + this.processDefinitionVersion = version; + return this; + } + + @Override + public DeploymentQuery processDefinitionType(String processDefinitionType) { + this.processDefinitionType = processDefinitionType; + return this; + } + + @Override + public DeploymentQuery processDefinitionType(boolean condition, String processDefinitionType) { + if (condition) { + this.processDefinitionType = processDefinitionType; + } + return this; + } + + @Override + public DeploymentQuery processDefinitionCode(String processDefinitionCode) { + this.processDefinitionCode = processDefinitionCode; + return this; + } + + @Override + public DeploymentQuery processDefinitionName(String processDefinitionName) { + this.processDefinitionName = processDefinitionName; + return this; + } + + @Override + public DeploymentQuery processDefinitionNameLike(String processDefinitionNameLike) { + this.processDefinitionNameLike = processDefinitionNameLike; + return this; + } + + @Override + public DeploymentQuery processDefinitionNameLike(boolean condition, String processDefinitionNameLike) { + if (condition) { + this.processDefinitionNameLike = processDefinitionNameLike; + } + return this; + } + + @Override + public DeploymentQuery processDefinitionDescLike(String processDefinitionDescLike) { + this.processDefinitionDescLike = processDefinitionDescLike; + return this; + } + + @Override + public DeploymentQuery deploymentUserId(String deploymentUserId) { + this.deploymentUserId = deploymentUserId; + return this; + } + + @Override + public DeploymentQuery deploymentStatus(String deploymentStatus) { + this.deploymentStatus = deploymentStatus; + return this; + } + + @Override + public DeploymentQuery deploymentStatus(boolean condition, String deploymentStatus) { + if (condition) { + this.deploymentStatus = deploymentStatus; + } + return this; + } + + @Override + public DeploymentQuery logicStatus(String logicStatus) { + this.logicStatus = logicStatus; + return this; + } + + // ============ Ordering ============ + + @Override + public DeploymentQuery orderByDeploymentId() { + return orderBy("id", "id"); + } + + @Override + public DeploymentQuery orderByCreateTime() { + return orderBy("gmtCreate", "gmt_create"); + } + + @Override + public DeploymentQuery orderByModifyTime() { + return orderBy("gmtModified", "gmt_modified"); + } + + @Override + public DeploymentQuery asc() { + return applyAsc(); + } + + @Override + public DeploymentQuery desc() { + return applyDesc(); + } + + // ============ Execution ============ + + @Override + protected List executeList() { + DeploymentInstanceQueryParam param = buildQueryParam(); + return deploymentInstanceStorage.findByPage(param, processEngineConfiguration); + } + + @Override + protected long executeCount() { + DeploymentInstanceQueryParam param = buildQueryParam(); + return deploymentInstanceStorage.count(param, processEngineConfiguration); + } + + /** + * Build DeploymentInstanceQueryParam from the fluent query settings. + */ + private DeploymentInstanceQueryParam buildQueryParam() { + DeploymentInstanceQueryParam param = new DeploymentInstanceQueryParam(); + + if (deploymentInstanceId != null) { + param.setId(Long.parseLong(deploymentInstanceId)); + } + + param.setProcessDefinitionVersion(processDefinitionVersion); + param.setProcessDefinitionType(processDefinitionType); + param.setProcessDefinitionCode(processDefinitionCode); + param.setProcessDefinitionName(processDefinitionName); + param.setProcessDefinitionNameLike(processDefinitionNameLike); + param.setProcessDefinitionDescLike(processDefinitionDescLike); + param.setDeploymentUserId(deploymentUserId); + param.setDeploymentStatus(deploymentStatus); + param.setLogicStatus(logicStatus); + + // Set pagination + param.setPageOffset(pageOffset); + param.setPageSize(pageSize); + if (!excludeTenant) { + param.setTenantId(tenantId); + } + + // Set order by specs + param.setOrderBySpecs(orderBySpecs); + + return param; + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/NotificationQueryImpl.java b/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/NotificationQueryImpl.java new file mode 100644 index 000000000..f68fe8ab8 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/NotificationQueryImpl.java @@ -0,0 +1,194 @@ +package com.alibaba.smart.framework.engine.query.impl; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.instance.storage.NotificationInstanceStorage; +import com.alibaba.smart.framework.engine.model.instance.NotificationInstance; +import com.alibaba.smart.framework.engine.query.NotificationQuery; +import com.alibaba.smart.framework.engine.service.param.query.NotificationQueryParam; + +/** + * Implementation of NotificationQuery fluent API. + * + * @author SmartEngine Team + */ +public class NotificationQueryImpl extends AbstractProcessBoundQuery + implements NotificationQuery { + + private NotificationInstanceStorage notificationInstanceStorage; + + // Filter conditions + private String notificationId; + private String senderUserId; + private String receiverUserId; + private String taskInstanceId; + private List taskInstanceIds; + private String notificationType; + private String readStatus; + private Date notificationStartTime; + private Date notificationEndTime; + + public NotificationQueryImpl(ProcessEngineConfiguration processEngineConfiguration) { + super(processEngineConfiguration); + this.notificationInstanceStorage = processEngineConfiguration.getAnnotationScanner() + .getExtensionPoint(ExtensionConstant.COMMON, NotificationInstanceStorage.class); + } + + // ============ Filter conditions ============ + + @Override + public NotificationQuery notificationId(String notificationId) { + this.notificationId = notificationId; + return this; + } + + @Override + public NotificationQuery senderUserId(String senderUserId) { + this.senderUserId = senderUserId; + return this; + } + + @Override + public NotificationQuery receiverUserId(String receiverUserId) { + this.receiverUserId = receiverUserId; + return this; + } + + @Override + public NotificationQuery receiverUserId(boolean condition, String receiverUserId) { + if (condition) { + this.receiverUserId = receiverUserId; + } + return this; + } + + @Override + public NotificationQuery taskInstanceId(String taskInstanceId) { + this.taskInstanceId = taskInstanceId; + return this; + } + + @Override + public NotificationQuery taskInstanceIdIn(List taskInstanceIds) { + this.taskInstanceIds = taskInstanceIds; + return this; + } + + @Override + public NotificationQuery notificationType(String notificationType) { + this.notificationType = notificationType; + return this; + } + + @Override + public NotificationQuery notificationType(boolean condition, String notificationType) { + if (condition) { + this.notificationType = notificationType; + } + return this; + } + + @Override + public NotificationQuery readStatus(String readStatus) { + this.readStatus = readStatus; + return this; + } + + @Override + public NotificationQuery readStatus(boolean condition, String readStatus) { + if (condition) { + this.readStatus = readStatus; + } + return this; + } + + @Override + public NotificationQuery notificationTimeAfter(Date startTime) { + this.notificationStartTime = startTime; + return this; + } + + @Override + public NotificationQuery notificationTimeBefore(Date endTime) { + this.notificationEndTime = endTime; + return this; + } + + // ============ Ordering ============ + + @Override + public NotificationQuery orderByNotificationId() { + return orderBy("id", "id"); + } + + @Override + public NotificationQuery orderByReadTime() { + return orderBy("readTime", "read_time"); + } + + // ============ Execution ============ + + @Override + protected List executeList() { + NotificationQueryParam param = buildQueryParam(); + return notificationInstanceStorage.findNotificationList(param, processEngineConfiguration); + } + + @Override + protected long executeCount() { + NotificationQueryParam param = buildQueryParam(); + Long count = notificationInstanceStorage.countNotifications(param, processEngineConfiguration); + return count != null ? count : 0L; + } + + /** + * Build NotificationQueryParam from the fluent query settings. + */ + private NotificationQueryParam buildQueryParam() { + NotificationQueryParam param = new NotificationQueryParam(); + + // Set ID filter + if (notificationId != null) { + param.setId(Long.parseLong(notificationId)); + } + + // Set user filters + param.setSenderUserId(senderUserId); + param.setReceiverUserId(receiverUserId); + + // Set task instance filter + if (taskInstanceId != null) { + List ids = new ArrayList<>(); + ids.add(taskInstanceId); + param.setTaskInstanceIdList(ids); + } else if (taskInstanceIds != null && !taskInstanceIds.isEmpty()) { + param.setTaskInstanceIdList(taskInstanceIds); + } + + // Set process instance filter (from base class) + List processInstanceIdList = buildProcessInstanceIdList(); + if (processInstanceIdList != null) { + param.setProcessInstanceIdList(processInstanceIdList); + } + + // Set other filters + param.setNotificationType(notificationType); + param.setReadStatus(readStatus); + param.setNotificationStartTime(notificationStartTime); + param.setNotificationEndTime(notificationEndTime); + + // Set pagination + param.setPageOffset(pageOffset); + param.setPageSize(pageSize); + param.setTenantId(tenantId); + + // Set order by specs + param.setOrderBySpecs(orderBySpecs); + + return param; + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/ProcessInstanceQueryImpl.java b/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/ProcessInstanceQueryImpl.java new file mode 100644 index 000000000..9c67eea36 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/ProcessInstanceQueryImpl.java @@ -0,0 +1,261 @@ +package com.alibaba.smart.framework.engine.query.impl; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.instance.storage.ProcessInstanceStorage; +import com.alibaba.smart.framework.engine.model.instance.ProcessInstance; +import com.alibaba.smart.framework.engine.query.ProcessInstanceQuery; +import com.alibaba.smart.framework.engine.service.param.query.ProcessInstanceQueryParam; + +/** + * Implementation of ProcessInstanceQuery fluent API. + * + * @author SmartEngine Team + */ +public class ProcessInstanceQueryImpl extends AbstractQuery + implements ProcessInstanceQuery { + + private ProcessInstanceStorage processInstanceStorage; + + // Filter conditions + private String processInstanceId; + private List processInstanceIds; + private String startUserId; + private String status; + private String processDefinitionType; + private String processDefinitionIdAndVersion; + private String parentInstanceId; + private String bizUniqueId; + private Date startTimeAfter; + private Date startTimeBefore; + private Date completeTimeStart; + private Date completeTimeEnd; + private List processDefinitionTypeList; + + public ProcessInstanceQueryImpl(ProcessEngineConfiguration processEngineConfiguration) { + super(processEngineConfiguration); + this.processInstanceStorage = processEngineConfiguration.getAnnotationScanner() + .getExtensionPoint(ExtensionConstant.COMMON, ProcessInstanceStorage.class); + } + + // ============ Filter conditions ============ + + @Override + public ProcessInstanceQuery processInstanceId(String processInstanceId) { + this.processInstanceId = processInstanceId; + return this; + } + + @Override + public ProcessInstanceQuery processInstanceId(boolean condition, String processInstanceId) { + if (condition) { + this.processInstanceId = processInstanceId; + } + return this; + } + + @Override + public ProcessInstanceQuery processInstanceIdIn(List processInstanceIds) { + this.processInstanceIds = processInstanceIds; + return this; + } + + @Override + public ProcessInstanceQuery startedBy(String startUserId) { + this.startUserId = startUserId; + return this; + } + + @Override + public ProcessInstanceQuery startedBy(boolean condition, String startUserId) { + if (condition) { + this.startUserId = startUserId; + } + return this; + } + + @Override + public ProcessInstanceQuery processStatus(String status) { + this.status = status; + return this; + } + + @Override + public ProcessInstanceQuery processStatus(boolean condition, String status) { + if (condition) { + this.status = status; + } + return this; + } + + @Override + public ProcessInstanceQuery processDefinitionType(String processDefinitionType) { + this.processDefinitionType = processDefinitionType; + return this; + } + + @Override + public ProcessInstanceQuery processDefinitionType(boolean condition, String processDefinitionType) { + if (condition) { + this.processDefinitionType = processDefinitionType; + } + return this; + } + + @Override + public ProcessInstanceQuery processDefinitionIdAndVersion(String processDefinitionIdAndVersion) { + this.processDefinitionIdAndVersion = processDefinitionIdAndVersion; + return this; + } + + @Override + public ProcessInstanceQuery parentInstanceId(String parentInstanceId) { + this.parentInstanceId = parentInstanceId; + return this; + } + + @Override + public ProcessInstanceQuery bizUniqueId(String bizUniqueId) { + this.bizUniqueId = bizUniqueId; + return this; + } + + @Override + public ProcessInstanceQuery bizUniqueId(boolean condition, String bizUniqueId) { + if (condition) { + this.bizUniqueId = bizUniqueId; + } + return this; + } + + @Override + public ProcessInstanceQuery startedAfter(Date startTimeAfter) { + this.startTimeAfter = startTimeAfter; + return this; + } + + @Override + public ProcessInstanceQuery startedBefore(Date startTimeBefore) { + this.startTimeBefore = startTimeBefore; + return this; + } + + @Override + public ProcessInstanceQuery completedAfter(Date completeTimeStart) { + this.completeTimeStart = completeTimeStart; + return this; + } + + @Override + public ProcessInstanceQuery completedBefore(Date completeTimeEnd) { + this.completeTimeEnd = completeTimeEnd; + return this; + } + + @Override + public ProcessInstanceQuery processDefinitionTypeIn(List types) { + this.processDefinitionTypeList = types != null ? new ArrayList(types) : null; + return this; + } + + @Override + public ProcessInstanceQuery involvedUser(String userId) { + this.startUserId = userId; + return this; + } + + // ============ Ordering ============ + + @Override + public ProcessInstanceQuery orderByProcessInstanceId() { + return orderBy("id", "id"); + } + + @Override + public ProcessInstanceQuery orderByStartTime() { + return orderBy("gmtCreate", "gmt_create"); + } + + @Override + public ProcessInstanceQuery orderByModifyTime() { + return orderBy("gmtModified", "gmt_modified"); + } + + @Override + public ProcessInstanceQuery orderByCompleteTime() { + return orderBy("completeTime", "complete_time"); + } + + @Override + public ProcessInstanceQuery asc() { + return applyAsc(); + } + + @Override + public ProcessInstanceQuery desc() { + return applyDesc(); + } + + // ============ Execution ============ + + @Override + protected List executeList() { + ProcessInstanceQueryParam param = buildQueryParam(); + return processInstanceStorage.queryProcessInstanceList(param, processEngineConfiguration); + } + + @Override + protected long executeCount() { + ProcessInstanceQueryParam param = buildQueryParam(); + Long count = processInstanceStorage.count(param, processEngineConfiguration); + return count != null ? count : 0L; + } + + /** + * Build ProcessInstanceQueryParam from the fluent query settings. + */ + private ProcessInstanceQueryParam buildQueryParam() { + ProcessInstanceQueryParam param = new ProcessInstanceQueryParam(); + + // Set ID filter + if (processInstanceId != null) { + List ids = new ArrayList<>(); + ids.add(processInstanceId); + param.setProcessInstanceIdList(ids); + } else if (processInstanceIds != null && !processInstanceIds.isEmpty()) { + param.setProcessInstanceIdList(processInstanceIds); + } + + // Set other filters + param.setStartUserId(startUserId); + param.setStatus(status); + param.setProcessDefinitionType(processDefinitionType); + param.setProcessDefinitionIdAndVersion(processDefinitionIdAndVersion); + param.setParentInstanceId(parentInstanceId); + param.setBizUniqueId(bizUniqueId); + param.setProcessStartTime(startTimeAfter); + param.setProcessEndTime(startTimeBefore); + param.setCompleteTimeStart(completeTimeStart); + param.setCompleteTimeEnd(completeTimeEnd); + // processDefinitionTypeList takes precedence over single type + if (processDefinitionTypeList != null && !processDefinitionTypeList.isEmpty()) { + param.setProcessDefinitionTypeList(processDefinitionTypeList); + } + + // Set pagination + param.setPageOffset(pageOffset); + param.setPageSize(pageSize); + if (!excludeTenant) { + param.setTenantId(tenantId); + } + + // Set order by specs + param.setOrderBySpecs(orderBySpecs); + + return param; + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/SupervisionQueryImpl.java b/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/SupervisionQueryImpl.java new file mode 100644 index 000000000..48ad63250 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/SupervisionQueryImpl.java @@ -0,0 +1,186 @@ +package com.alibaba.smart.framework.engine.query.impl; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.instance.storage.SupervisionInstanceStorage; +import com.alibaba.smart.framework.engine.model.instance.SupervisionInstance; +import com.alibaba.smart.framework.engine.query.SupervisionQuery; +import com.alibaba.smart.framework.engine.service.param.query.SupervisionQueryParam; + +/** + * Implementation of SupervisionQuery fluent API. + * + * @author SmartEngine Team + */ +public class SupervisionQueryImpl extends AbstractProcessBoundQuery + implements SupervisionQuery { + + private SupervisionInstanceStorage supervisionInstanceStorage; + + // Filter conditions + private String supervisionId; + private String supervisorUserId; + private String taskInstanceId; + private List taskInstanceIds; + private String supervisionType; + private String status; + private Date supervisionStartTime; + private Date supervisionEndTime; + + public SupervisionQueryImpl(ProcessEngineConfiguration processEngineConfiguration) { + super(processEngineConfiguration); + this.supervisionInstanceStorage = processEngineConfiguration.getAnnotationScanner() + .getExtensionPoint(ExtensionConstant.COMMON, SupervisionInstanceStorage.class); + } + + // ============ Filter conditions ============ + + @Override + public SupervisionQuery supervisionId(String supervisionId) { + this.supervisionId = supervisionId; + return this; + } + + @Override + public SupervisionQuery supervisorUserId(String supervisorUserId) { + this.supervisorUserId = supervisorUserId; + return this; + } + + @Override + public SupervisionQuery supervisorUserId(boolean condition, String supervisorUserId) { + if (condition) { + this.supervisorUserId = supervisorUserId; + } + return this; + } + + @Override + public SupervisionQuery taskInstanceId(String taskInstanceId) { + this.taskInstanceId = taskInstanceId; + return this; + } + + @Override + public SupervisionQuery taskInstanceIdIn(List taskInstanceIds) { + this.taskInstanceIds = taskInstanceIds; + return this; + } + + @Override + public SupervisionQuery supervisionType(String supervisionType) { + this.supervisionType = supervisionType; + return this; + } + + @Override + public SupervisionQuery supervisionType(boolean condition, String supervisionType) { + if (condition) { + this.supervisionType = supervisionType; + } + return this; + } + + @Override + public SupervisionQuery supervisionStatus(String status) { + this.status = status; + return this; + } + + @Override + public SupervisionQuery supervisionStatus(boolean condition, String status) { + if (condition) { + this.status = status; + } + return this; + } + + @Override + public SupervisionQuery supervisionTimeAfter(Date startTime) { + this.supervisionStartTime = startTime; + return this; + } + + @Override + public SupervisionQuery supervisionTimeBefore(Date endTime) { + this.supervisionEndTime = endTime; + return this; + } + + // ============ Ordering ============ + + @Override + public SupervisionQuery orderBySupervisionId() { + return orderBy("id", "id"); + } + + @Override + public SupervisionQuery orderByCloseTime() { + return orderBy("closeTime", "close_time"); + } + + // ============ Execution ============ + + @Override + protected List executeList() { + SupervisionQueryParam param = buildQueryParam(); + return supervisionInstanceStorage.findSupervisionList(param, processEngineConfiguration); + } + + @Override + protected long executeCount() { + SupervisionQueryParam param = buildQueryParam(); + Long count = supervisionInstanceStorage.countSupervision(param, processEngineConfiguration); + return count != null ? count : 0L; + } + + /** + * Build SupervisionQueryParam from the fluent query settings. + */ + private SupervisionQueryParam buildQueryParam() { + SupervisionQueryParam param = new SupervisionQueryParam(); + + // Set ID filter + if (supervisionId != null) { + param.setId(Long.parseLong(supervisionId)); + } + + // Set supervisor filter + param.setSupervisorUserId(supervisorUserId); + + // Set task instance filter + if (taskInstanceId != null) { + List ids = new ArrayList<>(); + ids.add(taskInstanceId); + param.setTaskInstanceIdList(ids); + } else if (taskInstanceIds != null && !taskInstanceIds.isEmpty()) { + param.setTaskInstanceIdList(taskInstanceIds); + } + + // Set process instance filter (from base class) + List processInstanceIdList = buildProcessInstanceIdList(); + if (processInstanceIdList != null) { + param.setProcessInstanceIdList(processInstanceIdList); + } + + // Set other filters + param.setSupervisionType(supervisionType); + param.setStatus(status); + param.setSupervisionStartTime(supervisionStartTime); + param.setSupervisionEndTime(supervisionEndTime); + + // Set pagination + param.setPageOffset(pageOffset); + param.setPageSize(pageSize); + param.setTenantId(tenantId); + + // Set order by specs + param.setOrderBySpecs(orderBySpecs); + + return param; + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/TaskQueryImpl.java b/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/TaskQueryImpl.java new file mode 100644 index 000000000..a090aec0d --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/query/impl/TaskQueryImpl.java @@ -0,0 +1,589 @@ +package com.alibaba.smart.framework.engine.query.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.dialect.Dialect; +import com.alibaba.smart.framework.engine.dialect.DialectRegistry; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.instance.storage.TaskInstanceStorage; +import com.alibaba.smart.framework.engine.model.instance.TaskInstance; +import com.alibaba.smart.framework.engine.query.TaskQuery; +import com.alibaba.smart.framework.engine.service.param.query.JsonCondition; +import com.alibaba.smart.framework.engine.service.param.query.JsonInCondition; +import com.alibaba.smart.framework.engine.service.param.query.TaskInstanceQueryByAssigneeParam; +import com.alibaba.smart.framework.engine.service.param.query.TaskInstanceQueryParam; + +/** + * Implementation of TaskQuery fluent API. + * + * @author SmartEngine Team + */ +public class TaskQueryImpl extends AbstractProcessBoundQuery implements TaskQuery { + + private TaskInstanceStorage taskInstanceStorage; + + // Filter conditions + private String taskInstanceId; + private String activityInstanceId; + private String processDefinitionType; + private String processDefinitionActivityId; + private String status; + private List statusList; + private String claimUserId; + private String tag; + private String extension; + private Integer priority; + private String comment; + private String title; + private Date completeTimeStart; + private Date completeTimeEnd; + private Date createTimeStart; + private Date createTimeEnd; + private String titleLike; + private boolean unassigned; + private List processDefinitionTypeList; + private String claimUserIdLike; + private Integer minPriority; + private Integer maxPriority; + private String domainCode; + private List domainCodeList; + private String domainCodeLike; + private Map jsonExactFilters = new LinkedHashMap(); + private Map> jsonInFilters = new LinkedHashMap>(); + private Map jsonLikeFilters = new LinkedHashMap(); + + private static final Pattern VALID_JSON_KEY = Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_.]*$"); + + // Candidate assignee filters (triggers assignee JOIN query path) + private String candidateUserId; + private List candidateGroupIds; + + public TaskQueryImpl(ProcessEngineConfiguration processEngineConfiguration) { + super(processEngineConfiguration); + this.taskInstanceStorage = processEngineConfiguration.getAnnotationScanner() + .getExtensionPoint(ExtensionConstant.COMMON, TaskInstanceStorage.class); + } + + // ============ Filter conditions ============ + + @Override + public TaskQuery taskInstanceId(String taskInstanceId) { + this.taskInstanceId = taskInstanceId; + return this; + } + + @Override + public TaskQuery activityInstanceId(String activityInstanceId) { + this.activityInstanceId = activityInstanceId; + return this; + } + + @Override + public TaskQuery processDefinitionType(String processDefinitionType) { + this.processDefinitionType = processDefinitionType; + return this; + } + + @Override + public TaskQuery processDefinitionType(boolean condition, String processDefinitionType) { + if (condition) { + this.processDefinitionType = processDefinitionType; + } + return this; + } + + @Override + public TaskQuery processDefinitionActivityId(String processDefinitionActivityId) { + this.processDefinitionActivityId = processDefinitionActivityId; + return this; + } + + @Override + public TaskQuery taskStatus(String status) { + this.status = status; + return this; + } + + @Override + public TaskQuery taskStatus(boolean condition, String status) { + if (condition) { + this.status = status; + } + return this; + } + + @Override + public TaskQuery taskStatusIn(List statuses) { + this.statusList = statuses; + return this; + } + + @Override + public TaskQuery taskStatusIn(String... statuses) { + this.statusList = Arrays.asList(statuses); + return this; + } + + @Override + public TaskQuery taskAssignee(String claimUserId) { + this.claimUserId = claimUserId; + return this; + } + + @Override + public TaskQuery taskAssignee(boolean condition, String claimUserId) { + if (condition) { + this.claimUserId = claimUserId; + } + return this; + } + + @Override + public TaskQuery taskCandidateUser(String userId) { + this.candidateUserId = userId; + return this; + } + + @Override + public TaskQuery taskCandidateGroup(String groupId) { + this.candidateGroupIds = Collections.singletonList(groupId); + return this; + } + + @Override + public TaskQuery taskCandidateGroupIn(List groupIds) { + this.candidateGroupIds = groupIds; + return this; + } + + @Override + public TaskQuery taskCandidateOrGroup(String userId, List groupIds) { + this.candidateUserId = userId; + this.candidateGroupIds = groupIds; + return this; + } + + @Override + public TaskQuery taskTag(String tag) { + this.tag = tag; + return this; + } + + @Override + public TaskQuery taskTag(boolean condition, String tag) { + if (condition) { + this.tag = tag; + } + return this; + } + + @Override + public TaskQuery taskExtension(String extension) { + this.extension = extension; + return this; + } + + @Override + public TaskQuery taskPriority(Integer priority) { + this.priority = priority; + return this; + } + + @Override + public TaskQuery taskComment(String comment) { + this.comment = comment; + return this; + } + + @Override + public TaskQuery taskTitle(String title) { + this.title = title; + return this; + } + + @Override + public TaskQuery taskTitle(boolean condition, String title) { + if (condition) { + this.title = title; + } + return this; + } + + @Override + public TaskQuery completeTimeAfter(Date completeTimeStart) { + this.completeTimeStart = completeTimeStart; + return this; + } + + @Override + public TaskQuery completeTimeBefore(Date completeTimeEnd) { + this.completeTimeEnd = completeTimeEnd; + return this; + } + + @Override + public TaskQuery createdAfter(Date createTimeStart) { + this.createTimeStart = createTimeStart; + return this; + } + + @Override + public TaskQuery createdBefore(Date createTimeEnd) { + this.createTimeEnd = createTimeEnd; + return this; + } + + @Override + public TaskQuery taskTitleLike(String titleLike) { + this.titleLike = titleLike; + return this; + } + + @Override + public TaskQuery taskTitleLike(boolean condition, String titleLike) { + if (condition) { + this.titleLike = titleLike; + } + return this; + } + + @Override + public TaskQuery taskUnassigned() { + this.unassigned = true; + return this; + } + + @Override + public TaskQuery processDefinitionTypeIn(List types) { + this.processDefinitionTypeList = types != null ? new ArrayList(types) : null; + return this; + } + + @Override + public TaskQuery taskAssigneeLike(String claimUserIdLike) { + this.claimUserIdLike = claimUserIdLike; + return this; + } + + @Override + public TaskQuery taskMinPriority(Integer minPriority) { + this.minPriority = minPriority; + return this; + } + + @Override + public TaskQuery taskMaxPriority(Integer maxPriority) { + this.maxPriority = maxPriority; + return this; + } + + // ============ Domain code filters ============ + + @Override + public TaskQuery domainCode(String domainCode) { + this.domainCode = domainCode; + return this; + } + + @Override + public TaskQuery domainCode(boolean condition, String domainCode) { + if (condition) { + this.domainCode = domainCode; + } + return this; + } + + @Override + public TaskQuery domainCodeIn(List domainCodes) { + this.domainCodeList = domainCodes != null ? new ArrayList(domainCodes) : null; + return this; + } + + @Override + public TaskQuery domainCodeLike(String domainCodeLike) { + this.domainCodeLike = domainCodeLike; + return this; + } + + // ============ Extra JSON filters ============ + + @Override + public TaskQuery extraJson(String key, String value) { + validateJsonKey(key); + jsonExactFilters.put(key, value); + return this; + } + + @Override + public TaskQuery extraJsonIn(String key, List values) { + validateJsonKey(key); + jsonInFilters.put(key, values); + return this; + } + + @Override + public TaskQuery extraJsonLike(String key, String pattern) { + validateJsonKey(key); + jsonLikeFilters.put(key, pattern); + return this; + } + + private void validateJsonKey(String key) { + if (key == null || !VALID_JSON_KEY.matcher(key).matches()) { + throw new IllegalArgumentException("Invalid JSON key: " + key + + ". Only [a-zA-Z_][a-zA-Z0-9_.]* is allowed."); + } + } + + // ============ Ordering ============ + + @Override + public TaskQuery orderByTaskId() { + return orderBy("id", "id"); + } + + @Override + public TaskQuery orderByClaimTime() { + return orderBy("claimTime", "claim_time"); + } + + @Override + public TaskQuery orderByCompleteTime() { + return orderBy("completeTime", "complete_time"); + } + + @Override + public TaskQuery orderByPriority() { + return orderBy("priority", "priority"); + } + + // ============ Execution ============ + + /** + * Check if candidate assignee filters are set, which requires the assignee JOIN query path. + */ + private boolean isCandidateQuery() { + return candidateUserId != null || (candidateGroupIds != null && !candidateGroupIds.isEmpty()); + } + + @Override + protected List executeList() { + if (isCandidateQuery()) { + TaskInstanceQueryByAssigneeParam param = buildAssigneeQueryParam(); + return taskInstanceStorage.findTaskListByAssignee(param, processEngineConfiguration); + } + TaskInstanceQueryParam param = buildQueryParam(); + return taskInstanceStorage.findTaskList(param, processEngineConfiguration); + } + + @Override + protected long executeCount() { + if (isCandidateQuery()) { + TaskInstanceQueryByAssigneeParam param = buildAssigneeQueryParam(); + Long count = taskInstanceStorage.countTaskListByAssignee(param, processEngineConfiguration); + return count != null ? count : 0L; + } + TaskInstanceQueryParam param = buildQueryParam(); + Long count = taskInstanceStorage.count(param, processEngineConfiguration); + return count != null ? count : 0L; + } + + /** + * Build TaskInstanceQueryByAssigneeParam for the assignee JOIN query path. + */ + private TaskInstanceQueryByAssigneeParam buildAssigneeQueryParam() { + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + + param.setAssigneeUserId(candidateUserId); + // Treat empty group list as null to avoid empty IN() SQL syntax error + param.setAssigneeGroupIdList( + candidateGroupIds != null && !candidateGroupIds.isEmpty() ? candidateGroupIds : null); + param.setProcessDefinitionType(processDefinitionType); + + // Set process instance ID list + List processInstanceIdList = buildProcessInstanceIdList(); + if (processInstanceIdList != null) { + List longIds = new ArrayList(processInstanceIdList.size()); + for (String id : processInstanceIdList) { + longIds.add(Long.parseLong(id)); + } + param.setProcessInstanceIdList(longIds); + } + + // statusList takes precedence, else single status + if (statusList != null && !statusList.isEmpty()) { + param.setStatus(statusList.get(0)); + } else { + param.setStatus(status); + } + + // domain_code filters + param.setDomainCode(domainCode); + param.setDomainCodeList(domainCodeList); + param.setDomainCodeLike(domainCodeLike); + + // Build JSON conditions + buildJsonConditionsForAssignee(param); + + // Set pagination and tenant + param.setPageOffset(pageOffset); + param.setPageSize(pageSize); + if (!excludeTenant) { + param.setTenantId(tenantId); + } + + return param; + } + + /** + * Build TaskInstanceQueryParam from the fluent query settings. + */ + private TaskInstanceQueryParam buildQueryParam() { + TaskInstanceQueryParam param = new TaskInstanceQueryParam(); + + // Set ID filter + if (taskInstanceId != null) { + param.setId(Long.parseLong(taskInstanceId)); + } + + // Set process instance filter (from base class) + List processInstanceIdList = buildProcessInstanceIdList(); + if (processInstanceIdList != null) { + param.setProcessInstanceIdList(processInstanceIdList); + } + + // Set other filters + param.setActivityInstanceId(activityInstanceId); + param.setProcessDefinitionType(processDefinitionType); + param.setProcessDefinitionActivityId(processDefinitionActivityId); + // statusList takes precedence over single status + if (statusList != null && !statusList.isEmpty()) { + param.setStatusList(statusList); + } else { + param.setStatus(status); + } + param.setClaimUserId(claimUserId); + param.setTag(tag); + param.setExtension(extension); + param.setPriority(priority); + param.setComment(comment); + param.setTitle(title); + param.setTitleLike(titleLike); + param.setCompleteTimeStart(completeTimeStart); + param.setCompleteTimeEnd(completeTimeEnd); + param.setCreateTimeStart(createTimeStart); + param.setCreateTimeEnd(createTimeEnd); + param.setUnassigned(unassigned ? Boolean.TRUE : null); + param.setClaimUserIdLike(claimUserIdLike); + param.setMinPriority(minPriority); + param.setMaxPriority(maxPriority); + // processDefinitionTypeList takes precedence over single type + if (processDefinitionTypeList != null && !processDefinitionTypeList.isEmpty()) { + param.setProcessDefinitionTypeList(processDefinitionTypeList); + } + + // domain_code filters + param.setDomainCode(domainCode); + param.setDomainCodeList(domainCodeList); + param.setDomainCodeLike(domainCodeLike); + + // Build JSON conditions + buildJsonConditions(param); + + // Set pagination + param.setPageOffset(pageOffset); + param.setPageSize(pageSize); + if (!excludeTenant) { + param.setTenantId(tenantId); + } + + // Set order by specs + param.setOrderBySpecs(orderBySpecs); + + return param; + } + + private Dialect resolveDialect() { + Dialect dialect = processEngineConfiguration.getDialect(); + if (dialect == null) { + dialect = DialectRegistry.getInstance().getDefaultDialect(); + } + return dialect; + } + + private void buildJsonConditions(TaskInstanceQueryParam param) { + if (jsonExactFilters.isEmpty() && jsonInFilters.isEmpty() && jsonLikeFilters.isEmpty()) { + return; + } + Dialect dialect = resolveDialect(); + + if (!jsonExactFilters.isEmpty()) { + List conditions = new ArrayList(); + for (Map.Entry e : jsonExactFilters.entrySet()) { + String expr = dialect.jsonExtractText("task.extra", e.getKey()); + conditions.add(new JsonCondition(expr, e.getValue())); + } + param.setJsonConditions(conditions); + } + + if (!jsonInFilters.isEmpty()) { + List inConditions = new ArrayList(); + for (Map.Entry> e : jsonInFilters.entrySet()) { + String expr = dialect.jsonExtractText("task.extra", e.getKey()); + inConditions.add(new JsonInCondition(expr, e.getValue())); + } + param.setJsonInConditions(inConditions); + } + + if (!jsonLikeFilters.isEmpty()) { + List likeConditions = new ArrayList(); + for (Map.Entry e : jsonLikeFilters.entrySet()) { + String expr = dialect.jsonExtractText("task.extra", e.getKey()); + likeConditions.add(new JsonCondition(expr, e.getValue())); + } + param.setJsonLikeConditions(likeConditions); + } + } + + private void buildJsonConditionsForAssignee(TaskInstanceQueryByAssigneeParam param) { + if (jsonExactFilters.isEmpty() && jsonInFilters.isEmpty() && jsonLikeFilters.isEmpty()) { + return; + } + Dialect dialect = resolveDialect(); + + if (!jsonExactFilters.isEmpty()) { + List conditions = new ArrayList(); + for (Map.Entry e : jsonExactFilters.entrySet()) { + String expr = dialect.jsonExtractText("task.extra", e.getKey()); + conditions.add(new JsonCondition(expr, e.getValue())); + } + param.setJsonConditions(conditions); + } + + if (!jsonInFilters.isEmpty()) { + List inConditions = new ArrayList(); + for (Map.Entry> e : jsonInFilters.entrySet()) { + String expr = dialect.jsonExtractText("task.extra", e.getKey()); + inConditions.add(new JsonInCondition(expr, e.getValue())); + } + param.setJsonInConditions(inConditions); + } + + if (!jsonLikeFilters.isEmpty()) { + List likeConditions = new ArrayList(); + for (Map.Entry e : jsonLikeFilters.entrySet()) { + String expr = dialect.jsonExtractText("task.extra", e.getKey()); + likeConditions.add(new JsonCondition(expr, e.getValue())); + } + param.setJsonLikeConditions(likeConditions); + } + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/command/NotificationCommandService.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/command/NotificationCommandService.java new file mode 100644 index 000000000..b909207ce --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/command/NotificationCommandService.java @@ -0,0 +1,63 @@ +package com.alibaba.smart.framework.engine.service.command; + +import java.util.List; + +import com.alibaba.smart.framework.engine.model.instance.NotificationInstance; + +/** + * 知会抄送命令服务接口 + * + * @author SmartEngine Team + */ +public interface NotificationCommandService { + + /** + * 发送知会通知 + * + * @param processInstanceId 流程实例ID + * @param taskInstanceId 任务实例ID(可选) + * @param senderUserId 发送人用户ID + * @param receiverUserIds 接收人用户ID列表 + * @param title 通知标题 + * @param content 通知内容 + * @param tenantId 租户ID + * @return 知会通知实例列表 + */ + List sendNotification(String processInstanceId, String taskInstanceId, + String senderUserId, List receiverUserIds, + String title, String content, String tenantId); + + /** + * 发送单个知会通知 + * + * @param processInstanceId 流程实例ID + * @param taskInstanceId 任务实例ID(可选) + * @param senderUserId 发送人用户ID + * @param receiverUserId 接收人用户ID + * @param title 通知标题 + * @param content 通知内容 + * @param notificationType 通知类型 + * @param tenantId 租户ID + * @return 知会通知实例 + */ + NotificationInstance sendSingleNotification(String processInstanceId, String taskInstanceId, + String senderUserId, String receiverUserId, + String title, String content, String notificationType, + String tenantId); + + /** + * 标记为已读 + * + * @param notificationId 通知ID + * @param tenantId 租户ID + */ + void markAsRead(String notificationId, String tenantId); + + /** + * 批量标记为已读 + * + * @param notificationIds 通知ID列表 + * @param tenantId 租户ID + */ + void batchMarkAsRead(List notificationIds, String tenantId); +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/command/ProcessCommandService.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/command/ProcessCommandService.java index 0c322596a..9cb298be0 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/service/command/ProcessCommandService.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/command/ProcessCommandService.java @@ -45,4 +45,21 @@ public interface ProcessCommandService { void abort(String processInstanceId, Map request); + /** + * Suspend a running process instance. Sets status to suspended without terminating + * active executions or tasks. + */ + void suspend(String processInstanceId, String tenantId); + + /** + * Resume a suspended process instance. Restores status to running. + */ + void resume(String processInstanceId, String tenantId); + + /** + * Jump to a specific node in the process instance. + * Aborts current active executions/tasks and creates a new execution at the target node. + */ + void jump(String processInstanceId, String targetNodeId, Map variables); + } diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/command/SupervisionCommandService.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/command/SupervisionCommandService.java new file mode 100644 index 000000000..3388566f7 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/command/SupervisionCommandService.java @@ -0,0 +1,56 @@ +package com.alibaba.smart.framework.engine.service.command; + +import java.util.List; + +import com.alibaba.smart.framework.engine.model.instance.SupervisionInstance; + +/** + * 督办管理命令服务接口 + * + * @author SmartEngine Team + */ +public interface SupervisionCommandService { + + /** + * 发起督办 + * + * @param taskInstanceId 任务实例ID + * @param supervisorUserId 督办人用户ID + * @param reason 督办原因 + * @param supervisionType 督办类型 + * @param tenantId 租户ID + * @return 督办实例 + */ + SupervisionInstance createSupervision(String taskInstanceId, String supervisorUserId, + String reason, String supervisionType, String tenantId); + + /** + * 关闭督办 + * + * @param supervisionId 督办ID + * @param tenantId 租户ID + */ + void closeSupervision(String supervisionId, String tenantId); + + /** + * 批量督办 + * + * @param taskInstanceIds 任务实例ID列表 + * @param supervisorUserId 督办人用户ID + * @param reason 督办原因 + * @param supervisionType 督办类型 + * @param tenantId 租户ID + * @return 督办实例列表 + */ + List batchCreateSupervision(List taskInstanceIds, + String supervisorUserId, String reason, + String supervisionType, String tenantId); + + /** + * 根据任务完成自动关闭督办 + * + * @param taskInstanceId 任务实例ID + * @param tenantId 租户ID + */ + void autoCloseSupervisionByTask(String taskInstanceId, String tenantId); +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/command/TaskCommandService.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/command/TaskCommandService.java index 90244136d..a3f767c98 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/service/command/TaskCommandService.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/command/TaskCommandService.java @@ -17,6 +17,11 @@ public interface TaskCommandService { ProcessInstance complete(String taskId, Map request); + /** + * Claim a task by setting the claimUserId. Only unclaimed tasks can be claimed. + */ + void claim(String taskId, String userId, String tenantId); + ProcessInstance complete(String taskId, String userId, Map request); ProcessInstance complete(String taskId, Map request, Map response); @@ -47,4 +52,24 @@ public interface TaskCommandService { */ void addTaskAssigneeCandidate(String taskId,String tenantId, TaskAssigneeCandidateInstance taskAssigneeCandidateInstance); + /** + * 增强的任务移交,支持原因和时限 + */ + void transferWithReason(String taskId, String fromUserId, String toUserId, String reason, String tenantId); + + /** + * 任务回退到指定节点 + */ + ProcessInstance rollbackTask(String taskId, String targetActivityId, String reason, String tenantId); + + /** + * 增强的加签操作,支持操作记录 + */ + void addTaskAssigneeCandidateWithReason(String taskId, String tenantId, TaskAssigneeCandidateInstance taskAssigneeCandidateInstance, String reason); + + /** + * 增强的减签操作,支持操作记录 + */ + void removeTaskAssigneeCandidateWithReason(String taskId, String tenantId, TaskAssigneeCandidateInstance taskAssigneeCandidateInstance, String reason); + } diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/command/impl/DefaultNotificationCommandService.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/command/impl/DefaultNotificationCommandService.java new file mode 100644 index 000000000..06a2bb282 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/command/impl/DefaultNotificationCommandService.java @@ -0,0 +1,128 @@ +package com.alibaba.smart.framework.engine.service.command.impl; + +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.aware.ProcessEngineConfigurationAware; +import com.alibaba.smart.framework.engine.configuration.scanner.AnnotationScanner; +import com.alibaba.smart.framework.engine.constant.NotificationConstant; +import com.alibaba.smart.framework.engine.exception.NotificationException; +import com.alibaba.smart.framework.engine.exception.ValidationException; +import com.alibaba.smart.framework.engine.extension.annotation.ExtensionBinding; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.hook.LifeCycleHook; +import com.alibaba.smart.framework.engine.instance.impl.DefaultNotificationInstance; +import com.alibaba.smart.framework.engine.instance.storage.NotificationInstanceStorage; +import com.alibaba.smart.framework.engine.model.instance.NotificationInstance; +import com.alibaba.smart.framework.engine.service.command.NotificationCommandService; + +/** + * 知会通知命令服务默认实现 + * + * @author SmartEngine Team + */ +@ExtensionBinding(group = ExtensionConstant.SERVICE, bindKey = NotificationCommandService.class) +public class DefaultNotificationCommandService implements NotificationCommandService, LifeCycleHook, ProcessEngineConfigurationAware { + + private ProcessEngineConfiguration processEngineConfiguration; + private NotificationInstanceStorage notificationInstanceStorage; + + @Override + public void start() { + AnnotationScanner annotationScanner = this.processEngineConfiguration.getAnnotationScanner(); + this.notificationInstanceStorage = annotationScanner.getExtensionPoint(ExtensionConstant.COMMON, NotificationInstanceStorage.class); + } + + @Override + public void stop() { + // 清理资源 + } + + @Override + public List sendNotification(String processInstanceId, String taskInstanceId, + String senderUserId, List receiverUserIds, + String title, String content, String tenantId) { + if (processInstanceId == null || senderUserId == null || receiverUserIds == null || receiverUserIds.isEmpty()) { + throw new ValidationException("ProcessInstanceId, SenderUserId and ReceiverUserIds cannot be null or empty"); + } + + List results = new ArrayList<>(); + for (String receiverUserId : receiverUserIds) { + try { + NotificationInstance notification = sendSingleNotification(processInstanceId, taskInstanceId, + senderUserId, receiverUserId, title, content, NotificationConstant.NotificationType.CC, tenantId); + results.add(notification); + } catch (Exception e) { + throw new NotificationException("Failed to send notification to user: " + receiverUserId, e); + } + } + return results; + } + + @Override + public NotificationInstance sendSingleNotification(String processInstanceId, String taskInstanceId, + String senderUserId, String receiverUserId, + String title, String content, String notificationType, + String tenantId) { + if (processInstanceId == null || senderUserId == null || receiverUserId == null) { + throw new ValidationException("ProcessInstanceId, SenderUserId and ReceiverUserId cannot be null"); + } + + if (notificationType == null || (!NotificationConstant.NotificationType.CC.equals(notificationType) + && !NotificationConstant.NotificationType.INFORM.equals(notificationType))) { + throw new ValidationException("Invalid notification type: " + notificationType); + } + + try { + NotificationInstance notificationInstance = new DefaultNotificationInstance(); + notificationInstance.setProcessInstanceId(processInstanceId); + notificationInstance.setTaskInstanceId(taskInstanceId); + notificationInstance.setSenderUserId(senderUserId); + notificationInstance.setReceiverUserId(receiverUserId); + notificationInstance.setNotificationType(notificationType); + notificationInstance.setTitle(title); + notificationInstance.setContent(content); + notificationInstance.setReadStatus(NotificationConstant.ReadStatus.UNREAD); + notificationInstance.setTenantId(tenantId); + + // 设置ID生成器 + processEngineConfiguration.getIdGenerator().generate(notificationInstance); + + return notificationInstanceStorage.insert(notificationInstance, processEngineConfiguration); + } catch (Exception e) { + throw new NotificationException("Failed to send notification", e); + } + } + + @Override + public void markAsRead(String notificationId, String tenantId) { + if (notificationId == null) { + throw new ValidationException("NotificationId cannot be null"); + } + + try { + notificationInstanceStorage.markAsRead(notificationId, tenantId, processEngineConfiguration); + } catch (Exception e) { + throw new NotificationException("Failed to mark notification as read: " + notificationId, e); + } + } + + @Override + public void batchMarkAsRead(List notificationIds, String tenantId) { + if (notificationIds == null || notificationIds.isEmpty()) { + throw new ValidationException("NotificationIds cannot be null or empty"); + } + + try { + notificationInstanceStorage.batchMarkAsRead(notificationIds, tenantId, processEngineConfiguration); + } catch (Exception e) { + throw new NotificationException("Failed to batch mark notifications as read", e); + } + } + + @Override + public void setProcessEngineConfiguration(ProcessEngineConfiguration processEngineConfiguration) { + this.processEngineConfiguration = processEngineConfiguration; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/command/impl/DefaultProcessCommandService.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/command/impl/DefaultProcessCommandService.java index 44ef5789f..02c2fc9a1 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/service/command/impl/DefaultProcessCommandService.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/command/impl/DefaultProcessCommandService.java @@ -27,6 +27,8 @@ import com.alibaba.smart.framework.engine.model.instance.InstanceStatus; import com.alibaba.smart.framework.engine.model.instance.ProcessInstance; import com.alibaba.smart.framework.engine.model.instance.TaskInstance; +import com.alibaba.smart.framework.engine.pvm.PvmActivity; +import com.alibaba.smart.framework.engine.pvm.PvmProcessDefinition; import com.alibaba.smart.framework.engine.pvm.PvmProcessInstance; import com.alibaba.smart.framework.engine.pvm.impl.DefaultPvmProcessInstance; import com.alibaba.smart.framework.engine.service.command.ProcessCommandService; @@ -229,6 +231,96 @@ public void abort(String processInstanceId, Map request) { } + @Override + public void suspend(String processInstanceId, String tenantId) { + ProcessInstance processInstance = processInstanceStorage.findOne(processInstanceId, tenantId, processEngineConfiguration); + if (processInstance == null) { + throw new IllegalArgumentException("ProcessInstance not found: " + processInstanceId); + } + if (processInstance.getStatus() != InstanceStatus.running) { + throw new IllegalStateException("Can only suspend a running process instance, current status: " + processInstance.getStatus()); + } + processInstance.setStatus(InstanceStatus.suspended); + processInstanceStorage.update(processInstance, processEngineConfiguration); + } + + @Override + public void resume(String processInstanceId, String tenantId) { + ProcessInstance processInstance = processInstanceStorage.findOne(processInstanceId, tenantId, processEngineConfiguration); + if (processInstance == null) { + throw new IllegalArgumentException("ProcessInstance not found: " + processInstanceId); + } + if (processInstance.getStatus() != InstanceStatus.suspended) { + throw new IllegalStateException("Can only resume a suspended process instance, current status: " + processInstance.getStatus()); + } + processInstance.setStatus(InstanceStatus.running); + processInstanceStorage.update(processInstance, processEngineConfiguration); + } + + @Override + public void jump(String processInstanceId, String targetNodeId, Map variables) { + String tenantId = null; + if (variables != null) { + tenantId = ObjectUtil.obj2Str(variables.get(RequestMapSpecialKeyConstant.TENANT_ID)); + } + + ProcessInstance processInstance = processInstanceStorage.findOne(processInstanceId, tenantId, processEngineConfiguration); + if (processInstance == null) { + throw new IllegalArgumentException("ProcessInstance not found: " + processInstanceId); + } + + // 1. Cancel all active executions + List activeExecutions = executionInstanceStorage.findActiveExecution( + processInstanceId, tenantId, processEngineConfiguration); + if (activeExecutions != null) { + for (ExecutionInstance exec : activeExecutions) { + MarkDoneUtil.markDoneExecutionInstance(exec, executionInstanceStorage, processEngineConfiguration); + } + } + + // 2. Cancel all pending tasks + TaskInstanceQueryParam taskQueryParam = new TaskInstanceQueryParam(); + List processInstanceIdList = new ArrayList<>(2); + processInstanceIdList.add(processInstanceId); + taskQueryParam.setProcessInstanceIdList(processInstanceIdList); + List taskInstances = taskInstanceStorage.findTaskList(taskQueryParam, processEngineConfiguration); + if (taskInstances != null) { + for (TaskInstance task : taskInstances) { + if (TaskInstanceConstant.COMPLETED.equals(task.getStatus()) + || TaskInstanceConstant.CANCELED.equals(task.getStatus()) + || TaskInstanceConstant.ABORTED.equals(task.getStatus())) { + continue; + } + MarkDoneUtil.markDoneTaskInstance(task, TaskInstanceConstant.CANCELED, TaskInstanceConstant.PENDING, + variables, taskInstanceStorage, processEngineConfiguration); + } + } + + // 3. Resolve target PvmActivity and jump to it + PvmProcessDefinition pvmProcessDefinition = processDefinitionContainer.getPvmProcessDefinition( + processInstance.getProcessDefinitionId(), processInstance.getProcessDefinitionVersion(), tenantId); + if (pvmProcessDefinition == null) { + throw new IllegalArgumentException("PvmProcessDefinition not found for process: " + + processInstance.getProcessDefinitionId()); + } + + Map activities = pvmProcessDefinition.getActivities(); + PvmActivity targetActivity = activities.get(targetNodeId); + if (targetActivity == null) { + throw new IllegalArgumentException("Target node not found in process definition: " + targetNodeId); + } + + if (variables == null) { + variables = new HashMap<>(); + } + + ExecutionContext executionContext = instanceContextFactory.createProcessContext( + processEngineConfiguration, processInstance, variables, null, null); + + PvmProcessInstance pvmProcessInstance = new DefaultPvmProcessInstance(); + pvmProcessInstance.jump(targetActivity, executionContext); + } + private ProcessEngineConfiguration processEngineConfiguration; private ProcessInstanceStorage processInstanceStorage; private TaskInstanceStorage taskInstanceStorage; diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/command/impl/DefaultSupervisionCommandService.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/command/impl/DefaultSupervisionCommandService.java new file mode 100644 index 000000000..341077458 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/command/impl/DefaultSupervisionCommandService.java @@ -0,0 +1,223 @@ +package com.alibaba.smart.framework.engine.service.command.impl; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.aware.ProcessEngineConfigurationAware; +import com.alibaba.smart.framework.engine.configuration.scanner.AnnotationScanner; +import com.alibaba.smart.framework.engine.constant.SupervisionConstant; +import com.alibaba.smart.framework.engine.exception.SupervisionException; +import com.alibaba.smart.framework.engine.exception.ValidationException; +import com.alibaba.smart.framework.engine.extension.annotation.ExtensionBinding; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.hook.LifeCycleHook; +import com.alibaba.smart.framework.engine.instance.impl.DefaultSupervisionInstance; +import com.alibaba.smart.framework.engine.instance.storage.SupervisionInstanceStorage; +import com.alibaba.smart.framework.engine.instance.storage.TaskInstanceStorage; +import com.alibaba.smart.framework.engine.model.instance.SupervisionInstance; +import com.alibaba.smart.framework.engine.model.instance.TaskInstance; +import com.alibaba.smart.framework.engine.service.command.NotificationCommandService; +import com.alibaba.smart.framework.engine.service.command.SupervisionCommandService; + +/** + * Default implementation of SupervisionCommandService. + * + * @author SmartEngine Team + */ +@ExtensionBinding(group = ExtensionConstant.SERVICE, bindKey = SupervisionCommandService.class) +public class DefaultSupervisionCommandService implements SupervisionCommandService, LifeCycleHook, + ProcessEngineConfigurationAware { + + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultSupervisionCommandService.class); + + private ProcessEngineConfiguration processEngineConfiguration; + private SupervisionInstanceStorage supervisionInstanceStorage; + private TaskInstanceStorage taskInstanceStorage; + private NotificationCommandService notificationCommandService; + + @Override + public void start() { + AnnotationScanner annotationScanner = this.processEngineConfiguration.getAnnotationScanner(); + this.supervisionInstanceStorage = annotationScanner.getExtensionPoint(ExtensionConstant.COMMON, + SupervisionInstanceStorage.class); + this.taskInstanceStorage = annotationScanner.getExtensionPoint(ExtensionConstant.COMMON, + TaskInstanceStorage.class); + this.notificationCommandService = annotationScanner.getExtensionPoint(ExtensionConstant.SERVICE, + NotificationCommandService.class); + } + + @Override + public void stop() { + // Clean up resources if needed + } + + @Override + public SupervisionInstance createSupervision(String taskInstanceId, String supervisorUserId, + String reason, String supervisionType, String tenantId) { + // Validate required parameters + if (taskInstanceId == null || supervisorUserId == null) { + throw new ValidationException("TaskInstanceId and SupervisorUserId cannot be null"); + } + + if (!isValidSupervisionType(supervisionType)) { + throw new ValidationException("Invalid supervision type: " + supervisionType); + } + + LOGGER.info("Creating supervision for task: {}, supervisor: {}, type: {}", + taskInstanceId, supervisorUserId, supervisionType); + + try { + // First, get task instance to retrieve processInstanceId (required for NOT NULL constraint) + TaskInstance taskInstance = taskInstanceStorage.find(taskInstanceId, tenantId, processEngineConfiguration); + if (taskInstance == null) { + throw new ValidationException("Task not found: " + taskInstanceId); + } + + SupervisionInstance supervisionInstance = new DefaultSupervisionInstance(); + supervisionInstance.setTaskInstanceId(taskInstanceId); + supervisionInstance.setProcessInstanceId(taskInstance.getProcessInstanceId()); // Set before insert + supervisionInstance.setSupervisorUserId(supervisorUserId); + supervisionInstance.setSupervisionReason(reason); + supervisionInstance.setSupervisionType(supervisionType); + supervisionInstance.setStatus(SupervisionConstant.SupervisionStatus.ACTIVE); + supervisionInstance.setTenantId(tenantId); + + // Generate ID + processEngineConfiguration.getIdGenerator().generate(supervisionInstance); + + // Save supervision record + SupervisionInstance result = supervisionInstanceStorage.insert(supervisionInstance, + processEngineConfiguration); + + // Update task priority and send notification + processTaskAfterSupervision(taskInstance, supervisorUserId, reason); + + LOGGER.info("Supervision created successfully: {}", result.getInstanceId()); + return result; + + } catch (ValidationException e) { + throw e; + } catch (Exception e) { + LOGGER.error("Failed to create supervision for task: {}", taskInstanceId, e); + throw new SupervisionException("Failed to create supervision", e); + } + } + + /** + * Process task after supervision is created: update priority and send notification. + */ + private void processTaskAfterSupervision(TaskInstance taskInstance, + String supervisorUserId, String reason) { + if (taskInstance == null) { + return; + } + + String tenantId = taskInstance.getTenantId(); + + // Increase task priority + int newPriority = (taskInstance.getPriority() != null ? taskInstance.getPriority() : 0) + 1; + taskInstance.setPriority(newPriority); + taskInstanceStorage.update(taskInstance, processEngineConfiguration); + LOGGER.debug("Task priority updated to {} for task: {}", newPriority, taskInstance.getInstanceId()); + + // Send supervision notification to task assignee + if (taskInstance.getClaimUserId() != null && notificationCommandService != null) { + try { + notificationCommandService.sendSingleNotification( + taskInstance.getProcessInstanceId(), + taskInstance.getInstanceId(), + supervisorUserId, + taskInstance.getClaimUserId(), + "任务督办通知", + "您的任务被督办,原因:" + reason, + "督办提醒", + tenantId + ); + LOGGER.debug("Supervision notification sent to user: {}", taskInstance.getClaimUserId()); + } catch (Exception e) { + LOGGER.warn("Failed to send supervision notification to user: {}", taskInstance.getClaimUserId(), e); + // Don't throw - notification failure should not fail the supervision creation + } + } + } + + @Override + public void closeSupervision(String supervisionId, String tenantId) { + if (supervisionId == null) { + throw new ValidationException("SupervisionId cannot be null"); + } + + LOGGER.info("Closing supervision: {}", supervisionId); + + try { + supervisionInstanceStorage.closeSupervision(supervisionId, tenantId, processEngineConfiguration); + LOGGER.info("Supervision closed successfully: {}", supervisionId); + } catch (Exception e) { + LOGGER.error("Failed to close supervision: {}", supervisionId, e); + throw new SupervisionException("Failed to close supervision: " + supervisionId, e); + } + } + + @Override + public List batchCreateSupervision(List taskInstanceIds, + String supervisorUserId, String reason, + String supervisionType, String tenantId) { + if (taskInstanceIds == null || taskInstanceIds.isEmpty()) { + throw new ValidationException("TaskInstanceIds cannot be null or empty"); + } + + LOGGER.info("Batch creating {} supervisions for supervisor: {}", taskInstanceIds.size(), supervisorUserId); + + List results = new ArrayList<>(); + for (String taskInstanceId : taskInstanceIds) { + try { + SupervisionInstance supervision = createSupervision(taskInstanceId, supervisorUserId, + reason, supervisionType, tenantId); + results.add(supervision); + } catch (Exception e) { + LOGGER.error("Failed to create supervision for task: {}", taskInstanceId, e); + throw new SupervisionException("Failed to create supervision for task: " + taskInstanceId, e); + } + } + + LOGGER.info("Batch supervision creation completed: {} created", results.size()); + return results; + } + + @Override + public void autoCloseSupervisionByTask(String taskInstanceId, String tenantId) { + if (taskInstanceId == null) { + throw new ValidationException("TaskInstanceId cannot be null"); + } + + LOGGER.info("Auto closing supervisions for task: {}", taskInstanceId); + + try { + supervisionInstanceStorage.closeSupervisionByTask(taskInstanceId, tenantId, processEngineConfiguration); + LOGGER.info("Supervisions auto closed for task: {}", taskInstanceId); + } catch (Exception e) { + LOGGER.error("Failed to auto close supervisions for task: {}", taskInstanceId, e); + throw new SupervisionException("Failed to auto close supervision by task: " + taskInstanceId, e); + } + } + + /** + * Check if supervision type is valid. + */ + private boolean isValidSupervisionType(String supervisionType) { + return supervisionType != null && ( + SupervisionConstant.SupervisionType.URGE.equals(supervisionType) + || SupervisionConstant.SupervisionType.TRACK.equals(supervisionType) + || SupervisionConstant.SupervisionType.REMIND.equals(supervisionType) + ); + } + + @Override + public void setProcessEngineConfiguration(ProcessEngineConfiguration processEngineConfiguration) { + this.processEngineConfiguration = processEngineConfiguration; + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/command/impl/DefaultTaskCommandService.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/command/impl/DefaultTaskCommandService.java index 7c567ae76..5256d00e6 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/service/command/impl/DefaultTaskCommandService.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/command/impl/DefaultTaskCommandService.java @@ -10,6 +10,8 @@ import com.alibaba.smart.framework.engine.common.util.InstanceUtil; import com.alibaba.smart.framework.engine.common.util.MarkDoneUtil; import com.alibaba.smart.framework.engine.configuration.ConfigurationOption; +import com.alibaba.smart.framework.engine.configuration.TaskEventPublisher; +import com.alibaba.smart.framework.engine.pvm.event.EventConstant; import com.alibaba.smart.framework.engine.configuration.IdGenerator; import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; import com.alibaba.smart.framework.engine.configuration.aware.ProcessEngineConfigurationAware; @@ -23,15 +25,24 @@ import com.alibaba.smart.framework.engine.hook.LifeCycleHook; import com.alibaba.smart.framework.engine.instance.impl.DefaultTaskAssigneeInstance; import com.alibaba.smart.framework.engine.instance.impl.DefaultTaskInstance; +import com.alibaba.smart.framework.engine.instance.impl.DefaultTaskTransferRecord; +import com.alibaba.smart.framework.engine.instance.impl.DefaultAssigneeOperationRecord; +import com.alibaba.smart.framework.engine.instance.impl.DefaultRollbackRecord; import com.alibaba.smart.framework.engine.instance.storage.ActivityInstanceStorage; import com.alibaba.smart.framework.engine.instance.storage.ExecutionInstanceStorage; import com.alibaba.smart.framework.engine.instance.storage.ProcessInstanceStorage; import com.alibaba.smart.framework.engine.instance.storage.TaskAssigneeStorage; import com.alibaba.smart.framework.engine.instance.storage.TaskInstanceStorage; +import com.alibaba.smart.framework.engine.instance.storage.TaskTransferRecordStorage; +import com.alibaba.smart.framework.engine.instance.storage.AssigneeOperationRecordStorage; +import com.alibaba.smart.framework.engine.instance.storage.RollbackRecordStorage; +import com.alibaba.smart.framework.engine.instance.storage.SupervisionInstanceStorage; import com.alibaba.smart.framework.engine.model.instance.*; import com.alibaba.smart.framework.engine.service.command.ExecutionCommandService; import com.alibaba.smart.framework.engine.service.command.TaskCommandService; import com.alibaba.smart.framework.engine.util.ObjectUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * @author 高海军 帝奇 2016.11.11 @@ -41,6 +52,8 @@ public class DefaultTaskCommandService implements TaskCommandService, LifeCycleHook , ProcessEngineConfigurationAware { + private static final Logger logger = LoggerFactory.getLogger(DefaultTaskCommandService.class); + private ProcessInstanceStorage processInstanceStorage; private ActivityInstanceStorage activityInstanceStorage; private ExecutionInstanceStorage executionInstanceStorage; @@ -48,6 +61,9 @@ public class DefaultTaskCommandService implements TaskCommandService, LifeCycleH private ProcessEngineConfiguration processEngineConfiguration; private TaskInstanceStorage taskInstanceStorage; private TaskAssigneeStorage taskAssigneeStorage; + private TaskTransferRecordStorage taskTransferRecordStorage; + private AssigneeOperationRecordStorage assigneeOperationRecordStorage; + private RollbackRecordStorage rollbackRecordStorage; @Override public void start() { @@ -60,6 +76,11 @@ public void start() { this.executionInstanceStorage = annotationScanner.getExtensionPoint(ExtensionConstant.COMMON,ExecutionInstanceStorage.class); this.taskInstanceStorage = annotationScanner.getExtensionPoint(ExtensionConstant.COMMON,TaskInstanceStorage.class); + // Initialize Storage for operation records + this.taskTransferRecordStorage = annotationScanner.getExtensionPoint(ExtensionConstant.COMMON, TaskTransferRecordStorage.class); + this.assigneeOperationRecordStorage = annotationScanner.getExtensionPoint(ExtensionConstant.COMMON, AssigneeOperationRecordStorage.class); + this.rollbackRecordStorage = annotationScanner.getExtensionPoint(ExtensionConstant.COMMON, RollbackRecordStorage.class); + } @Override @@ -68,6 +89,22 @@ public void stop() { } + @Override + public void claim(String taskId, String userId, String tenantId) { + TaskInstance taskInstance = taskInstanceStorage.find(taskId, tenantId, processEngineConfiguration); + if (taskInstance == null) { + throw new ValidationException("Task instance not found for taskId: " + taskId); + } + if (taskInstance.getClaimUserId() != null && !taskInstance.getClaimUserId().isEmpty()) { + throw new ValidationException("Task already claimed by user: " + taskInstance.getClaimUserId()); + } + taskInstance.setClaimUserId(userId); + taskInstance.setClaimTime(DateUtil.getCurrentDate()); + taskInstanceStorage.update(taskInstance, processEngineConfiguration); + fireTaskEvent(EventConstant.TASK_CLAIMED, taskInstance, tenantId, + Map.of("claimUserId", userId)); + } + @Override public ProcessInstance complete(String taskId, Map request, Map response) { @@ -79,6 +116,20 @@ public ProcessInstance complete(String taskId, Map request, Map< MarkDoneUtil.markDoneTaskInstance(taskInstance, TaskInstanceConstant.COMPLETED, TaskInstanceConstant.PENDING, request, taskInstanceStorage, processEngineConfiguration); + fireTaskEvent(EventConstant.TASK_COMPLETED, taskInstance, tenantId, Map.of()); + + // 自动关闭该任务的所有督办 + try { + SupervisionInstanceStorage supervisionStorage = (SupervisionInstanceStorage) + processEngineConfiguration.getInstanceAccessor().access("supervisionInstanceStorage"); + if (supervisionStorage != null) { + supervisionStorage.closeSupervisionByTask(taskInstance.getInstanceId(), tenantId, processEngineConfiguration); + } + } catch (Exception e) { + // 记录日志但不影响任务完成 + logger.warn("Failed to close supervision for task: " + taskId, e); + } + return executionCommandService.signal(taskInstance.getExecutionInstanceId(), request,response); } @@ -103,6 +154,9 @@ public ProcessInstance complete(String taskId, String userId, Map request) { public void setProcessEngineConfiguration(ProcessEngineConfiguration processEngineConfiguration) { this.processEngineConfiguration = processEngineConfiguration; } + + @Override + public void transferWithReason(String taskId, String fromUserId, String toUserId, String reason, String tenantId) { + // 执行原有的转交逻辑 + transfer(taskId, fromUserId, toUserId, tenantId); + + // 记录转交操作 + IdGenerator idGenerator = processEngineConfiguration.getIdGenerator(); + DefaultTaskTransferRecord record = new DefaultTaskTransferRecord(); + idGenerator.generate(record); + record.setTaskInstanceId(taskId); + record.setFromUserId(fromUserId); + record.setToUserId(toUserId); + record.setTransferReason(reason); + record.setTenantId(tenantId); + + taskTransferRecordStorage.insert(record, processEngineConfiguration); + TaskInstance taskInstance = taskInstanceStorage.find(taskId, tenantId, processEngineConfiguration); + fireTaskEvent(EventConstant.TASK_TRANSFERRED, taskInstance, tenantId, + Map.of("fromUserId", fromUserId, "toUserId", toUserId, "reason", reason != null ? reason : "")); + } + + @Override + public ProcessInstance rollbackTask(String taskId, String targetActivityId, String reason, String tenantId) { + TaskInstance taskInstance = taskInstanceStorage.find(taskId, tenantId, processEngineConfiguration); + + if (taskInstance == null) { + throw new ValidationException("Task instance not found for taskId: " + taskId); + } + + // 获取流程实例 + ProcessInstance processInstance = processInstanceStorage.findOne( + taskInstance.getProcessInstanceId(), tenantId, processEngineConfiguration); + + String currentActivityId = taskInstance.getProcessDefinitionActivityId(); + + // 记录回退操作(在执行回退之前) + IdGenerator idGenerator = processEngineConfiguration.getIdGenerator(); + DefaultRollbackRecord record = new DefaultRollbackRecord(); + idGenerator.generate(record); + record.setProcessInstanceId(processInstance.getInstanceId()); + record.setTaskInstanceId(taskInstance.getInstanceId()); + record.setRollbackType("specific"); + record.setFromActivityId(currentActivityId); + record.setToActivityId(targetActivityId); + record.setOperatorUserId(taskInstance.getClaimUserId()); // 使用当前任务处理人作为操作人 + record.setRollbackReason(reason); + record.setTenantId(tenantId); + + rollbackRecordStorage.insert(record, processEngineConfiguration); + + // 执行回退 + return executionCommandService.jumpTo( + taskInstance.getProcessInstanceId(), + processInstance.getProcessDefinitionId(), + processInstance.getProcessDefinitionVersion(), + processInstance.getStatus(), + targetActivityId, + tenantId + ); + } + + @Override + public void addTaskAssigneeCandidateWithReason(String taskId, String tenantId, TaskAssigneeCandidateInstance taskAssigneeCandidateInstance, String reason) { + // 执行原有的加签逻辑 + addTaskAssigneeCandidate(taskId, tenantId, taskAssigneeCandidateInstance); + + // 记录加签操作 + IdGenerator idGenerator = processEngineConfiguration.getIdGenerator(); + DefaultAssigneeOperationRecord record = new DefaultAssigneeOperationRecord(); + idGenerator.generate(record); + record.setTaskInstanceId(taskId); + + // 获取任务实例以获取操作人 + TaskInstance taskInstance = taskInstanceStorage.find(taskId, tenantId, processEngineConfiguration); + if (taskInstance != null) { + record.setOperatorUserId(taskInstance.getClaimUserId()); + } + + record.setOperationType("add_assignee"); + record.setTargetUserId(taskAssigneeCandidateInstance.getAssigneeId()); + record.setOperationReason(reason); + record.setTenantId(tenantId); + + assigneeOperationRecordStorage.insert(record, processEngineConfiguration); + if (taskInstance != null) { + fireTaskEvent(EventConstant.TASK_DELEGATED, taskInstance, tenantId, + Map.of("newAssigneeId", taskAssigneeCandidateInstance.getAssigneeId(), + "reason", reason != null ? reason : "")); + } + } + + @Override + public void removeTaskAssigneeCandidateWithReason(String taskId, String tenantId, TaskAssigneeCandidateInstance taskAssigneeCandidateInstance, String reason) { + // 执行原有的减签逻辑 + removeTaskAssigneeCandidate(taskId, tenantId, taskAssigneeCandidateInstance); + + // 记录减签操作 + IdGenerator idGenerator = processEngineConfiguration.getIdGenerator(); + DefaultAssigneeOperationRecord record = new DefaultAssigneeOperationRecord(); + idGenerator.generate(record); + record.setTaskInstanceId(taskId); + + // 获取任务实例以获取操作人 + TaskInstance taskInstance = taskInstanceStorage.find(taskId, tenantId, processEngineConfiguration); + if (taskInstance != null) { + record.setOperatorUserId(taskInstance.getClaimUserId()); + } + + record.setOperationType("remove_assignee"); + record.setTargetUserId(taskAssigneeCandidateInstance.getAssigneeId()); + record.setOperationReason(reason); + record.setTenantId(tenantId); + + assigneeOperationRecordStorage.insert(record, processEngineConfiguration); + if (taskInstance != null) { + fireTaskEvent(EventConstant.TASK_REVOKED, taskInstance, tenantId, + Map.of("removedAssigneeId", taskAssigneeCandidateInstance.getAssigneeId(), + "reason", reason != null ? reason : "")); + } + } + + private void fireTaskEvent(EventConstant event, TaskInstance taskInstance, + String tenantId, Map extra) { + TaskEventPublisher publisher = processEngineConfiguration.getTaskEventPublisher(); + if (publisher != null) { + publisher.publish(event, taskInstance, tenantId, extra != null ? extra : Map.of()); + } + } } diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/CompletedProcessQueryParam.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/CompletedProcessQueryParam.java new file mode 100644 index 000000000..730f9045d --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/CompletedProcessQueryParam.java @@ -0,0 +1,62 @@ +package com.alibaba.smart.framework.engine.service.param.query; + +import java.util.Date; +import java.util.List; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 办结流程查询参数 + * + * @author SmartEngine Team + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class CompletedProcessQueryParam extends BaseQueryParam { + + /** + * 参与用户ID(发起人或处理过任务的用户) + */ + private String participantUserId; + + /** + * 流程定义类型列表 + */ + private List processDefinitionTypes; + + /** + * 完成时间开始 + */ + private Date completeTimeStart; + + /** + * 完成时间结束 + */ + private Date completeTimeEnd; + + /** + * 流程实例ID列表 + */ + private List processInstanceIdList; + + /** + * 流程标题(模糊查询) + */ + private String title; + + /** + * 流程标签 + */ + private String tag; + + /** + * 业务唯一ID + */ + private String bizUniqueId; + + /** + * 发起人用户ID + */ + private String startUserId; +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/CompletedTaskQueryParam.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/CompletedTaskQueryParam.java new file mode 100644 index 000000000..e189a6f38 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/CompletedTaskQueryParam.java @@ -0,0 +1,57 @@ +package com.alibaba.smart.framework.engine.service.param.query; + +import java.util.Date; +import java.util.List; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 已办任务查询参数 + * + * @author SmartEngine Team + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class CompletedTaskQueryParam extends BaseQueryParam { + + /** + * 任务处理人用户ID + */ + private String claimUserId; + + /** + * 流程定义类型列表 + */ + private List processDefinitionTypes; + + /** + * 完成时间开始 + */ + private Date completeTimeStart; + + /** + * 完成时间结束 + */ + private Date completeTimeEnd; + + /** + * 流程实例ID列表 + */ + private List processInstanceIdList; + + /** + * 任务标题(模糊查询) + */ + private String title; + + /** + * 任务标签 + */ + private String tag; + + /** + * 处理意见(模糊查询) + */ + private String comment; +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/JsonCondition.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/JsonCondition.java new file mode 100644 index 000000000..7c843e18d --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/JsonCondition.java @@ -0,0 +1,23 @@ +package com.alibaba.smart.framework.engine.service.param.query; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * Represents a JSON key-value condition for SQL WHERE clause. + * The sqlExpression is generated by Dialect.jsonExtractText() and is safe from injection. + */ +@Data +@AllArgsConstructor +public class JsonCondition { + + /** + * Dialect-generated SQL expression, e.g. "JSON_UNQUOTE(JSON_EXTRACT(task.extra, '$.category'))" + */ + private String sqlExpression; + + /** + * The comparison value (parameterized via #{}). + */ + private String value; +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/JsonInCondition.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/JsonInCondition.java new file mode 100644 index 000000000..926cc492a --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/JsonInCondition.java @@ -0,0 +1,25 @@ +package com.alibaba.smart.framework.engine.service.param.query; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * Represents a JSON key IN (...) condition for SQL WHERE clause. + * The sqlExpression is generated by Dialect.jsonExtractText() and is safe from injection. + */ +@Data +@AllArgsConstructor +public class JsonInCondition { + + /** + * Dialect-generated SQL expression, e.g. "JSON_UNQUOTE(JSON_EXTRACT(task.extra, '$.category'))" + */ + private String sqlExpression; + + /** + * The list of values to match against. + */ + private List values; +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/NotificationQueryParam.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/NotificationQueryParam.java new file mode 100644 index 000000000..1383b5740 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/NotificationQueryParam.java @@ -0,0 +1,59 @@ +package com.alibaba.smart.framework.engine.service.param.query; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * Notification query parameter. + * + * @author SmartEngine Team + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class NotificationQueryParam extends BaseQueryParam { + + /** + * Sender user ID + */ + private String senderUserId; + + /** + * Receiver user ID + */ + private String receiverUserId; + + /** + * Process instance ID list + */ + private List processInstanceIdList; + + /** + * Task instance ID list + */ + private List taskInstanceIdList; + + /** + * Notification type + */ + private String notificationType; + + /** + * Read status + */ + private String readStatus; + + /** + * Notification start time + */ + private Date notificationStartTime; + + /** + * Notification end time + */ + private Date notificationEndTime; + +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/PaginateQueryParam.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/PaginateQueryParam.java index 90e6c0ca1..088fbb530 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/PaginateQueryParam.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/PaginateQueryParam.java @@ -1,5 +1,9 @@ package com.alibaba.smart.framework.engine.service.param.query; +import java.util.List; + +import com.alibaba.smart.framework.engine.query.OrderSpec; + import lombok.Data; /** @@ -15,4 +19,10 @@ public class PaginateQueryParam { protected Integer pageOffset; protected Integer pageSize; + /** + * Dynamic order by specifications for fluent query API. + * When set, this will be used instead of default ordering in SQL. + */ + protected List orderBySpecs; + } diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/ProcessInstanceQueryParam.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/ProcessInstanceQueryParam.java index 75738548b..21aeef64a 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/ProcessInstanceQueryParam.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/ProcessInstanceQueryParam.java @@ -1,5 +1,6 @@ package com.alibaba.smart.framework.engine.service.param.query; +import java.io.Serializable; import java.util.Date; import java.util.List; @@ -19,13 +20,13 @@ public class ProcessInstanceQueryParam extends BaseQueryParam { private String startUserId; private String status ; private String processDefinitionType; - private String parentInstanceId; + private Serializable parentInstanceId; private String bizUniqueId; private String processDefinitionIdAndVersion; /** * 流程引擎实例id列表 */ - private List processInstanceIdList; + private List processInstanceIdList; /** * 查询启动时间在processStartTime之后的流程实例 @@ -36,4 +37,20 @@ public class ProcessInstanceQueryParam extends BaseQueryParam { * 查询启动时间在processEndTime之前的流程实例 */ private Date processEndTime; + + /** + * 完成时间开始 + */ + private Date completeTimeStart; + + /** + * 完成时间结束 + */ + private Date completeTimeEnd; + + /** + * Filter by multiple process definition types (IN clause). + */ + private List processDefinitionTypeList; + } diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/SupervisionQueryParam.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/SupervisionQueryParam.java new file mode 100644 index 000000000..416ba375c --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/SupervisionQueryParam.java @@ -0,0 +1,54 @@ +package com.alibaba.smart.framework.engine.service.param.query; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * Supervision query parameter. + * + * @author SmartEngine Team + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class SupervisionQueryParam extends BaseQueryParam { + + /** + * Supervisor user ID + */ + private String supervisorUserId; + + /** + * Task instance ID list + */ + private List taskInstanceIdList; + + /** + * Process instance ID list + */ + private List processInstanceIdList; + + /** + * Supervision type + */ + private String supervisionType; + + /** + * Supervision status + */ + private String status; + + /** + * Supervision start time + */ + private Date supervisionStartTime; + + /** + * Supervision end time + */ + private Date supervisionEndTime; + +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/TaskInstanceQueryByAssigneeParam.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/TaskInstanceQueryByAssigneeParam.java index fd7a427b4..6f37431c1 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/TaskInstanceQueryByAssigneeParam.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/TaskInstanceQueryByAssigneeParam.java @@ -4,6 +4,8 @@ import lombok.Data; import lombok.EqualsAndHashCode; +import com.alibaba.smart.framework.engine.service.param.query.JsonCondition; +import com.alibaba.smart.framework.engine.service.param.query.JsonInCondition; /** * Created by jerry.zzy on 2017/11/16. @@ -22,4 +24,20 @@ public class TaskInstanceQueryByAssigneeParam extends BaseQueryParam { private String status; + // ============ domain_code filters ============ + + private String domainCode; + + private List domainCodeList; + + private String domainCodeLike; + + // ============ extra JSON conditions (built by Dialect) ============ + + private List jsonConditions; + + private List jsonInConditions; + + private List jsonLikeConditions; + } diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/TaskInstanceQueryParam.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/TaskInstanceQueryParam.java index 9716dcb66..0202daff1 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/TaskInstanceQueryParam.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/param/query/TaskInstanceQueryParam.java @@ -1,19 +1,22 @@ package com.alibaba.smart.framework.engine.service.param.query; +import java.io.Serializable; import java.util.Date; import java.util.List; import lombok.Data; import lombok.EqualsAndHashCode; +import com.alibaba.smart.framework.engine.service.param.query.JsonCondition; +import com.alibaba.smart.framework.engine.service.param.query.JsonInCondition; @Data @EqualsAndHashCode(callSuper = true) public class TaskInstanceQueryParam extends BaseQueryParam { - private List processInstanceIdList; + private List processInstanceIdList; - private String activityInstanceId; + private Serializable activityInstanceId; private String processDefinitionType; @@ -23,6 +26,11 @@ public class TaskInstanceQueryParam extends BaseQueryParam { */ private String status; + /** + * Filter by multiple task statuses. + */ + private List statusList; + private String claimUserId; private String tag; @@ -35,4 +43,70 @@ public class TaskInstanceQueryParam extends BaseQueryParam { private String title; -} \ No newline at end of file + /** + * Fuzzy search on title (LIKE %titleLike%). + */ + private String titleLike; + + /** + * 完成时间开始 + */ + private Date completeTimeStart; + + /** + * 完成时间结束 + */ + private Date completeTimeEnd; + + /** + * Filter by create time range (start, inclusive). + */ + private Date createTimeStart; + + /** + * Filter by create time range (end, exclusive). + */ + private Date createTimeEnd; + + /** + * Filter by multiple process definition types (IN clause). + */ + private List processDefinitionTypeList; + + /** + * Filter for unassigned tasks (claim_user_id IS NULL). + */ + private Boolean unassigned; + + /** + * Fuzzy search on claim user ID (LIKE %claimUserIdLike%). + */ + private String claimUserIdLike; + + /** + * Filter by minimum priority (inclusive). + */ + private Integer minPriority; + + /** + * Filter by maximum priority (inclusive). + */ + private Integer maxPriority; + + // ============ domain_code filters ============ + + private String domainCode; + + private List domainCodeList; + + private String domainCodeLike; + + // ============ extra JSON conditions (built by Dialect) ============ + + private List jsonConditions; + + private List jsonInConditions; + + private List jsonLikeConditions; + +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/query/DeploymentQueryService.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/query/DeploymentQueryService.java index 3ba13fa76..0b427382a 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/service/query/DeploymentQueryService.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/query/DeploymentQueryService.java @@ -12,10 +12,27 @@ */ public interface DeploymentQueryService { + /** + * @deprecated Use {@code smartEngine.createDeploymentQuery().deploymentInstanceId(id).singleResult()} instead + */ + @Deprecated DeploymentInstance findById(String deploymentInstanceId); + + /** + * @deprecated Use {@code smartEngine.createDeploymentQuery().deploymentInstanceId(id).tenantId(t).singleResult()} instead + */ + @Deprecated DeploymentInstance findById(String deploymentInstanceId,String tenantId); + /** + * @deprecated Use {@code smartEngine.createDeploymentQuery()...list()} instead + */ + @Deprecated List findList(DeploymentInstanceQueryParam deploymentInstanceQueryParam) ; + /** + * @deprecated Use {@code smartEngine.createDeploymentQuery()...count()} instead + */ + @Deprecated Integer count(DeploymentInstanceQueryParam deploymentInstanceQueryParam); } diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/query/NotificationQueryService.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/query/NotificationQueryService.java new file mode 100644 index 000000000..bd7d2b3e8 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/query/NotificationQueryService.java @@ -0,0 +1,85 @@ +package com.alibaba.smart.framework.engine.service.query; + +import java.util.List; + +import com.alibaba.smart.framework.engine.model.instance.NotificationInstance; +import com.alibaba.smart.framework.engine.service.param.query.NotificationQueryParam; + +/** + * 知会抄送查询服务接口 + * + * @author SmartEngine Team + */ +public interface NotificationQueryService { + + /** + * 查询知会通知列表 + * + * @param param 查询参数 + * @return 知会通知列表 + * @deprecated Use {@code smartEngine.createNotificationQuery()} fluent API instead + */ + @Deprecated + List findNotificationList(NotificationQueryParam param); + + /** + * 统计知会通知数量 + * + * @param param 查询参数 + * @return 知会通知数量 + * @deprecated Use {@code smartEngine.createNotificationQuery()...count()} instead + */ + @Deprecated + Long countNotifications(NotificationQueryParam param); + + /** + * 统计未读通知数量 + * + * @param receiverUserId 接收人用户ID + * @param tenantId 租户ID + * @return 未读通知数量 + * @deprecated Use {@code smartEngine.createNotificationQuery().receiverUserId(u).readStatus("unread").tenantId(t).count()} instead + */ + @Deprecated + Long countUnreadNotifications(String receiverUserId, String tenantId); + + /** + * 根据ID查询知会通知 + * + * @param notificationId 通知ID + * @param tenantId 租户ID + * @return 知会通知 + * @deprecated Use {@code smartEngine.createNotificationQuery().notificationId(id).tenantId(t).singleResult()} instead + */ + @Deprecated + NotificationInstance findOne(String notificationId, String tenantId); + + /** + * 查询接收人的知会列表 + * + * @param receiverUserId 接收人用户ID + * @param readStatus 读取状态(可选) + * @param tenantId 租户ID + * @param pageOffset 分页偏移 + * @param pageSize 分页大小 + * @return 知会通知列表 + * @deprecated Use {@code smartEngine.createNotificationQuery().receiverUserId(u).readStatus(s).tenantId(t).listPage(offset, size)} instead + */ + @Deprecated + List findByReceiver(String receiverUserId, String readStatus, + String tenantId, Integer pageOffset, Integer pageSize); + + /** + * 查询发送人的知会列表 + * + * @param senderUserId 发送人用户ID + * @param tenantId 租户ID + * @param pageOffset 分页偏移 + * @param pageSize 分页大小 + * @return 知会通知列表 + * @deprecated Use {@code smartEngine.createNotificationQuery().senderUserId(u).tenantId(t).listPage(offset, size)} instead + */ + @Deprecated + List findBySender(String senderUserId, String tenantId, + Integer pageOffset, Integer pageSize); +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/query/ProcessQueryService.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/query/ProcessQueryService.java index f00d2b8a6..0817e6cd4 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/service/query/ProcessQueryService.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/query/ProcessQueryService.java @@ -4,6 +4,7 @@ import com.alibaba.smart.framework.engine.model.instance.ProcessInstance; import com.alibaba.smart.framework.engine.service.param.query.ProcessInstanceQueryParam; +import com.alibaba.smart.framework.engine.service.param.query.CompletedProcessQueryParam; /** * 查询流程实例。 @@ -12,12 +13,48 @@ */ public interface ProcessQueryService { + /** + * @deprecated Use {@code smartEngine.createProcessQuery().processInstanceId(id).singleResult()} instead + */ + @Deprecated ProcessInstance findById(String processInstanceId); + /** + * @deprecated Use {@code smartEngine.createProcessQuery().processInstanceId(id).tenantId(t).singleResult()} instead + */ + @Deprecated ProcessInstance findById(String processInstanceId,String tenantId); + /** + * @deprecated Use {@code smartEngine.createProcessQuery()} fluent API instead + */ + @Deprecated List findList(ProcessInstanceQueryParam processInstanceQueryParam); + /** + * @deprecated Use {@code smartEngine.createProcessQuery()...count()} instead + */ + @Deprecated Long count(ProcessInstanceQueryParam processInstanceQueryParam); + /** + * 查询办结流程列表 - 基于现有findList方法扩展 + * + * @param param 办结流程查询参数 + * @return 办结流程列表 + * @deprecated Use {@code smartEngine.createProcessQuery().involvedUser(u).processDefinitionTypeIn(types).completedAfter(s).completedBefore(e).processStatus("completed").list()} instead + */ + @Deprecated + List findCompletedProcessList(CompletedProcessQueryParam param); + + /** + * 统计办结流程数量 + * + * @param param 办结流程查询参数 + * @return 办结流程数量 + * @deprecated Use {@code smartEngine.createProcessQuery().involvedUser(u).processDefinitionTypeIn(types).completedAfter(s).completedBefore(e).processStatus("completed").count()} instead + */ + @Deprecated + Long countCompletedProcessList(CompletedProcessQueryParam param); + } diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/query/SupervisionQueryService.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/query/SupervisionQueryService.java new file mode 100644 index 000000000..2e92b9a2a --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/query/SupervisionQueryService.java @@ -0,0 +1,56 @@ +package com.alibaba.smart.framework.engine.service.query; + +import java.util.List; + +import com.alibaba.smart.framework.engine.model.instance.SupervisionInstance; +import com.alibaba.smart.framework.engine.service.param.query.SupervisionQueryParam; + +/** + * 督办管理查询服务接口 + * + * @author SmartEngine Team + */ +public interface SupervisionQueryService { + + /** + * 查询督办记录列表 + * + * @param param 查询参数 + * @return 督办记录列表 + * @deprecated Use {@code smartEngine.createSupervisionQuery()} fluent API instead + */ + @Deprecated + List findSupervisionList(SupervisionQueryParam param); + + /** + * 统计督办记录数量 + * + * @param param 查询参数 + * @return 督办记录数量 + * @deprecated Use {@code smartEngine.createSupervisionQuery()...count()} instead + */ + @Deprecated + Long countSupervision(SupervisionQueryParam param); + + /** + * 查询任务的活跃督办记录 + * + * @param taskInstanceId 任务实例ID + * @param tenantId 租户ID + * @return 活跃督办记录列表 + * @deprecated Use {@code smartEngine.createSupervisionQuery().taskInstanceId(id).supervisionStatus("active").tenantId(t).list()} instead + */ + @Deprecated + List findActiveSupervisionByTask(String taskInstanceId, String tenantId); + + /** + * 根据ID查询督办记录 + * + * @param supervisionId 督办ID + * @param tenantId 租户ID + * @return 督办记录 + * @deprecated Use {@code smartEngine.createSupervisionQuery().supervisionId(id).tenantId(t).singleResult()} instead + */ + @Deprecated + SupervisionInstance findOne(String supervisionId, String tenantId); +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/query/TaskQueryService.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/query/TaskQueryService.java index 0fda4daa3..4f0a11c19 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/service/query/TaskQueryService.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/query/TaskQueryService.java @@ -6,6 +6,7 @@ import com.alibaba.smart.framework.engine.service.param.query.PendingTaskQueryParam; import com.alibaba.smart.framework.engine.service.param.query.TaskInstanceQueryByAssigneeParam; import com.alibaba.smart.framework.engine.service.param.query.TaskInstanceQueryParam; +import com.alibaba.smart.framework.engine.service.param.query.CompletedTaskQueryParam; /** * 用户任务查询服务。 @@ -16,13 +17,28 @@ public interface TaskQueryService { /** * 待办任务列表 + * + * @deprecated Use {@code smartEngine.createTaskQuery().taskCandidateOrGroup(userId, groupIds).taskStatus("pending").list()} instead */ + @Deprecated List findPendingTaskList(PendingTaskQueryParam pendingTaskQueryParam); + /** + * @deprecated Use {@code smartEngine.createTaskQuery().taskCandidateOrGroup(userId, groupIds).taskStatus("pending").count()} instead + */ + @Deprecated Long countPendingTaskList(PendingTaskQueryParam pendingTaskQueryParam); + /** + * @deprecated Use {@code smartEngine.createTaskQuery().taskCandidateOrGroup(userId, groupIds).taskStatus(s).list()} instead + */ + @Deprecated List findTaskListByAssignee(TaskInstanceQueryByAssigneeParam param); + /** + * @deprecated Use {@code smartEngine.createTaskQuery().taskCandidateOrGroup(userId, groupIds).taskStatus(s).count()} instead + */ + @Deprecated Long countTaskListByAssignee(TaskInstanceQueryByAssigneeParam param); /** @@ -30,19 +46,61 @@ public interface TaskQueryService { * * @param processInstanceId * @return + * @deprecated Use {@code smartEngine.createTaskQuery().processInstanceId(id).taskStatus("pending").list()} instead */ + @Deprecated List findAllPendingTaskList(String processInstanceId); + + /** + * @deprecated Use {@code smartEngine.createTaskQuery().processInstanceId(id).taskStatus("pending").tenantId(t).list()} instead + */ + @Deprecated List findAllPendingTaskList(String processInstanceId,String tenantId); + /** + * @deprecated Use {@code smartEngine.createTaskQuery().taskInstanceId(id).singleResult()} instead + */ + @Deprecated TaskInstance findOne(String taskInstanceId); + + /** + * @deprecated Use {@code smartEngine.createTaskQuery().taskInstanceId(id).tenantId(t).singleResult()} instead + */ + @Deprecated TaskInstance findOne(String taskInstanceId,String tenantId); /** * 扩展方法,可用于典型的审批场景等等,取决于tag的值是什么。tag 任意非null值,可以为 appproved,rejected 等等。 * + * @deprecated Use {@code smartEngine.createTaskQuery()} fluent API instead */ + @Deprecated List findList(TaskInstanceQueryParam taskInstanceQueryParam); + /** + * @deprecated Use {@code smartEngine.createTaskQuery()...count()} instead + */ + @Deprecated Long count(TaskInstanceQueryParam taskInstanceQueryParam); + /** + * 查询已办任务列表 - 基于现有findList方法扩展 + * + * @param param 已办任务查询参数 + * @return 已办任务列表 + * @deprecated Use {@code smartEngine.createTaskQuery().taskAssignee(u).processDefinitionTypeIn(types).completeTimeAfter(s).completeTimeBefore(e).taskStatus("completed").list()} instead + */ + @Deprecated + List findCompletedTaskList(CompletedTaskQueryParam param); + + /** + * 统计已办任务数量 + * + * @param param 已办任务查询参数 + * @return 已办任务数量 + * @deprecated Use {@code smartEngine.createTaskQuery().taskAssignee(u).processDefinitionTypeIn(types).completeTimeAfter(s).completeTimeBefore(e).taskStatus("completed").count()} instead + */ + @Deprecated + Long countCompletedTaskList(CompletedTaskQueryParam param); + } diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/query/impl/DefaultNotificationQueryService.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/query/impl/DefaultNotificationQueryService.java new file mode 100644 index 000000000..e299e6cd3 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/query/impl/DefaultNotificationQueryService.java @@ -0,0 +1,131 @@ +package com.alibaba.smart.framework.engine.service.query.impl; + +import java.util.List; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.aware.ProcessEngineConfigurationAware; +import com.alibaba.smart.framework.engine.configuration.scanner.AnnotationScanner; +import com.alibaba.smart.framework.engine.constant.NotificationConstant; +import com.alibaba.smart.framework.engine.exception.NotificationException; +import com.alibaba.smart.framework.engine.exception.ValidationException; +import com.alibaba.smart.framework.engine.extension.annotation.ExtensionBinding; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.hook.LifeCycleHook; +import com.alibaba.smart.framework.engine.instance.storage.NotificationInstanceStorage; +import com.alibaba.smart.framework.engine.model.instance.NotificationInstance; +import com.alibaba.smart.framework.engine.service.param.query.NotificationQueryParam; +import com.alibaba.smart.framework.engine.service.query.NotificationQueryService; + +/** + * 知会通知查询服务默认实现 + * + * @author SmartEngine Team + */ +@ExtensionBinding(group = ExtensionConstant.SERVICE, bindKey = NotificationQueryService.class) +public class DefaultNotificationQueryService implements NotificationQueryService, LifeCycleHook, ProcessEngineConfigurationAware { + + private ProcessEngineConfiguration processEngineConfiguration; + private NotificationInstanceStorage notificationInstanceStorage; + + @Override + public void start() { + AnnotationScanner annotationScanner = this.processEngineConfiguration.getAnnotationScanner(); + this.notificationInstanceStorage = annotationScanner.getExtensionPoint(ExtensionConstant.COMMON, NotificationInstanceStorage.class); + } + + @Override + public void stop() { + // 清理资源 + } + + @Override + public NotificationInstance findOne(String notificationId, String tenantId) { + if (notificationId == null) { + throw new ValidationException("NotificationId cannot be null"); + } + + try { + return notificationInstanceStorage.find(notificationId, tenantId, processEngineConfiguration); + } catch (Exception e) { + throw new NotificationException("Failed to find notification: " + notificationId, e); + } + } + + @Override + public List findNotificationList(NotificationQueryParam param) { + if (param == null) { + throw new ValidationException("NotificationQueryParam cannot be null"); + } + + try { + return notificationInstanceStorage.findNotificationList(param, processEngineConfiguration); + } catch (Exception e) { + throw new NotificationException("Failed to find notification list", e); + } + } + + @Override + public Long countNotifications(NotificationQueryParam param) { + if (param == null) { + throw new ValidationException("NotificationQueryParam cannot be null"); + } + + try { + return notificationInstanceStorage.countNotifications(param, processEngineConfiguration); + } catch (Exception e) { + throw new NotificationException("Failed to count notifications", e); + } + } + + @Override + public Long countUnreadNotifications(String receiverUserId, String tenantId) { + if (receiverUserId == null) { + throw new ValidationException("ReceiverUserId cannot be null"); + } + + try { + return notificationInstanceStorage.countUnreadNotifications(receiverUserId, tenantId, processEngineConfiguration); + } catch (Exception e) { + throw new NotificationException("Failed to count unread notifications for user: " + receiverUserId, e); + } + } + + @Override + public List findByReceiver(String receiverUserId, String readStatus, + String tenantId, Integer pageOffset, Integer pageSize) { + if (receiverUserId == null) { + throw new ValidationException("ReceiverUserId cannot be null"); + } + + try { + return notificationInstanceStorage.findByReceiver(receiverUserId, readStatus, tenantId, + pageOffset != null ? pageOffset : 0, + pageSize != null ? pageSize : 20, + processEngineConfiguration); + } catch (Exception e) { + throw new NotificationException("Failed to find notifications by receiver: " + receiverUserId, e); + } + } + + @Override + public List findBySender(String senderUserId, String tenantId, + Integer pageOffset, Integer pageSize) { + if (senderUserId == null) { + throw new ValidationException("SenderUserId cannot be null"); + } + + try { + return notificationInstanceStorage.findBySender(senderUserId, tenantId, + pageOffset != null ? pageOffset : 0, + pageSize != null ? pageSize : 20, + processEngineConfiguration); + } catch (Exception e) { + throw new NotificationException("Failed to find notifications by sender: " + senderUserId, e); + } + } + + @Override + public void setProcessEngineConfiguration(ProcessEngineConfiguration processEngineConfiguration) { + this.processEngineConfiguration = processEngineConfiguration; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/query/impl/DefaultProcessQueryService.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/query/impl/DefaultProcessQueryService.java index df3823ab6..3a7b58021 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/service/query/impl/DefaultProcessQueryService.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/query/impl/DefaultProcessQueryService.java @@ -1,5 +1,6 @@ package com.alibaba.smart.framework.engine.service.query.impl; +import java.util.Collections; import java.util.List; import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; @@ -8,61 +9,108 @@ import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; import com.alibaba.smart.framework.engine.hook.LifeCycleHook; import com.alibaba.smart.framework.engine.instance.storage.ProcessInstanceStorage; +import com.alibaba.smart.framework.engine.model.instance.InstanceStatus; import com.alibaba.smart.framework.engine.model.instance.ProcessInstance; +import com.alibaba.smart.framework.engine.service.param.query.CompletedProcessQueryParam; import com.alibaba.smart.framework.engine.service.param.query.ProcessInstanceQueryParam; import com.alibaba.smart.framework.engine.service.query.ProcessQueryService; /** - * Created by 高海军 帝奇 74394 on 2016 November 22:10. + * Default implementation of ProcessQueryService. + * + * @author 高海军 帝奇 */ - @ExtensionBinding(group = ExtensionConstant.SERVICE, bindKey = ProcessQueryService.class) +public class DefaultProcessQueryService implements ProcessQueryService, LifeCycleHook, + ProcessEngineConfigurationAware { -public class DefaultProcessQueryService implements ProcessQueryService, LifeCycleHook , - ProcessEngineConfigurationAware { - + private ProcessEngineConfiguration processEngineConfiguration; private ProcessInstanceStorage processInstanceStorage; - - @Override public void start() { - - this.processInstanceStorage = processEngineConfiguration.getAnnotationScanner().getExtensionPoint(ExtensionConstant.COMMON,ProcessInstanceStorage.class); - - + this.processInstanceStorage = processEngineConfiguration.getAnnotationScanner() + .getExtensionPoint(ExtensionConstant.COMMON, ProcessInstanceStorage.class); } @Override public void stop() { - + // Clean up resources if needed } @Override public ProcessInstance findById(String processInstanceId) { + return findById(processInstanceId, null); + } - return findById(processInstanceId,null); + @Override + public ProcessInstance findById(String processInstanceId, String tenantId) { + return processInstanceStorage.findOne(processInstanceId, tenantId, processEngineConfiguration); } @Override - public ProcessInstance findById(String processInstanceId,String tenantId) { + public List findList(ProcessInstanceQueryParam processInstanceQueryParam) { + return processInstanceStorage.queryProcessInstanceList(processInstanceQueryParam, processEngineConfiguration); + } - return processInstanceStorage.findOne(processInstanceId,tenantId, processEngineConfiguration); + @Override + public Long count(ProcessInstanceQueryParam processInstanceQueryParam) { + return processInstanceStorage.count(processInstanceQueryParam, processEngineConfiguration); } @Override - public List findList(ProcessInstanceQueryParam processInstanceQueryParam) { + public List findCompletedProcessList(CompletedProcessQueryParam param) { + if (param == null) { + return Collections.emptyList(); + } + + ProcessInstanceQueryParam processInstanceQueryParam = convertToProcessInstanceQueryParam(param); + processInstanceQueryParam.setStatus(InstanceStatus.completed.name()); return processInstanceStorage.queryProcessInstanceList(processInstanceQueryParam, processEngineConfiguration); } @Override - public Long count(ProcessInstanceQueryParam processInstanceQueryParam) { + public Long countCompletedProcessList(CompletedProcessQueryParam param) { + if (param == null) { + return 0L; + } + + ProcessInstanceQueryParam processInstanceQueryParam = convertToProcessInstanceQueryParam(param); + processInstanceQueryParam.setStatus(InstanceStatus.completed.name()); - return processInstanceStorage.count(processInstanceQueryParam,processEngineConfiguration ); + return processInstanceStorage.count(processInstanceQueryParam, processEngineConfiguration); } - private ProcessEngineConfiguration processEngineConfiguration; + /** + * Convert CompletedProcessQueryParam to ProcessInstanceQueryParam. + * Note: For completed process query, we only filter by completeTime, not processStartTime. + */ + private ProcessInstanceQueryParam convertToProcessInstanceQueryParam(CompletedProcessQueryParam param) { + ProcessInstanceQueryParam processInstanceQueryParam = new ProcessInstanceQueryParam(); + + // Pagination + processInstanceQueryParam.setTenantId(param.getTenantId()); + processInstanceQueryParam.setPageOffset(param.getPageOffset()); + processInstanceQueryParam.setPageSize(param.getPageSize()); + + // Query conditions + processInstanceQueryParam.setStartUserId(param.getStartUserId()); + processInstanceQueryParam.setProcessInstanceIdList(param.getProcessInstanceIdList()); + processInstanceQueryParam.setBizUniqueId(param.getBizUniqueId()); + + // Complete time filter (NOT processStartTime - that was a bug) + processInstanceQueryParam.setCompleteTimeStart(param.getCompleteTimeStart()); + processInstanceQueryParam.setCompleteTimeEnd(param.getCompleteTimeEnd()); + + // Process definition type (take first one if multiple provided) + // TODO: Consider extending ProcessInstanceQueryParam to support multiple types + if (param.getProcessDefinitionTypes() != null && !param.getProcessDefinitionTypes().isEmpty()) { + processInstanceQueryParam.setProcessDefinitionType(param.getProcessDefinitionTypes().get(0)); + } + + return processInstanceQueryParam; + } @Override public void setProcessEngineConfiguration(ProcessEngineConfiguration processEngineConfiguration) { diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/query/impl/DefaultSupervisionQueryService.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/query/impl/DefaultSupervisionQueryService.java new file mode 100644 index 000000000..d447f737f --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/query/impl/DefaultSupervisionQueryService.java @@ -0,0 +1,96 @@ +package com.alibaba.smart.framework.engine.service.query.impl; + +import java.util.List; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.aware.ProcessEngineConfigurationAware; +import com.alibaba.smart.framework.engine.configuration.scanner.AnnotationScanner; +import com.alibaba.smart.framework.engine.exception.SupervisionException; +import com.alibaba.smart.framework.engine.exception.ValidationException; +import com.alibaba.smart.framework.engine.extension.annotation.ExtensionBinding; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.hook.LifeCycleHook; +import com.alibaba.smart.framework.engine.instance.storage.SupervisionInstanceStorage; +import com.alibaba.smart.framework.engine.model.instance.SupervisionInstance; +import com.alibaba.smart.framework.engine.service.param.query.SupervisionQueryParam; +import com.alibaba.smart.framework.engine.service.query.SupervisionQueryService; + +/** + * 督办查询服务默认实现 + * + * @author SmartEngine Team + */ +@ExtensionBinding(group = ExtensionConstant.SERVICE, bindKey = SupervisionQueryService.class) +public class DefaultSupervisionQueryService implements SupervisionQueryService, LifeCycleHook, ProcessEngineConfigurationAware { + + private ProcessEngineConfiguration processEngineConfiguration; + private SupervisionInstanceStorage supervisionInstanceStorage; + + @Override + public void start() { + AnnotationScanner annotationScanner = this.processEngineConfiguration.getAnnotationScanner(); + this.supervisionInstanceStorage = annotationScanner.getExtensionPoint(ExtensionConstant.COMMON, SupervisionInstanceStorage.class); + } + + @Override + public void stop() { + // 清理资源 + } + + @Override + public List findSupervisionList(SupervisionQueryParam param) { + if (param == null) { + throw new ValidationException("SupervisionQueryParam cannot be null"); + } + + try { + return supervisionInstanceStorage.findSupervisionList(param, processEngineConfiguration); + } catch (Exception e) { + throw new SupervisionException("Failed to find supervision list", e); + } + } + + @Override + public Long countSupervision(SupervisionQueryParam param) { + if (param == null) { + throw new ValidationException("SupervisionQueryParam cannot be null"); + } + + try { + return supervisionInstanceStorage.countSupervision(param, processEngineConfiguration); + } catch (Exception e) { + throw new SupervisionException("Failed to count supervision", e); + } + } + + @Override + public List findActiveSupervisionByTask(String taskInstanceId, String tenantId) { + if (taskInstanceId == null) { + throw new ValidationException("TaskInstanceId cannot be null"); + } + + try { + return supervisionInstanceStorage.findActiveSupervisionByTask(taskInstanceId, tenantId, processEngineConfiguration); + } catch (Exception e) { + throw new SupervisionException("Failed to find active supervision by task: " + taskInstanceId, e); + } + } + + @Override + public SupervisionInstance findOne(String supervisionId, String tenantId) { + if (supervisionId == null) { + throw new ValidationException("SupervisionId cannot be null"); + } + + try { + return supervisionInstanceStorage.find(supervisionId, tenantId, processEngineConfiguration); + } catch (Exception e) { + throw new SupervisionException("Failed to find supervision: " + supervisionId, e); + } + } + + @Override + public void setProcessEngineConfiguration(ProcessEngineConfiguration processEngineConfiguration) { + this.processEngineConfiguration = processEngineConfiguration; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/service/query/impl/DefaultTaskQueryService.java b/core/src/main/java/com/alibaba/smart/framework/engine/service/query/impl/DefaultTaskQueryService.java index 88aa21d47..276af36d1 100644 --- a/core/src/main/java/com/alibaba/smart/framework/engine/service/query/impl/DefaultTaskQueryService.java +++ b/core/src/main/java/com/alibaba/smart/framework/engine/service/query/impl/DefaultTaskQueryService.java @@ -1,6 +1,7 @@ package com.alibaba.smart.framework.engine.service.query.impl; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; @@ -11,46 +12,42 @@ import com.alibaba.smart.framework.engine.hook.LifeCycleHook; import com.alibaba.smart.framework.engine.instance.storage.TaskInstanceStorage; import com.alibaba.smart.framework.engine.model.instance.TaskInstance; +import com.alibaba.smart.framework.engine.service.param.query.CompletedTaskQueryParam; import com.alibaba.smart.framework.engine.service.param.query.PendingTaskQueryParam; import com.alibaba.smart.framework.engine.service.param.query.TaskInstanceQueryByAssigneeParam; import com.alibaba.smart.framework.engine.service.param.query.TaskInstanceQueryParam; import com.alibaba.smart.framework.engine.service.query.TaskQueryService; /** - * Created by 高海军 帝奇 74394 on 2016 November 22:10. + * Default implementation of TaskQueryService. + * + * @author 高海军 帝奇 */ @ExtensionBinding(group = ExtensionConstant.SERVICE, bindKey = TaskQueryService.class) +public class DefaultTaskQueryService implements TaskQueryService, LifeCycleHook, + ProcessEngineConfigurationAware { -public class DefaultTaskQueryService implements TaskQueryService, LifeCycleHook , - ProcessEngineConfigurationAware { - - private ProcessEngineConfiguration processEngineConfiguration ; + private ProcessEngineConfiguration processEngineConfiguration; private TaskInstanceStorage taskInstanceStorage; @Override public void start() { - this.taskInstanceStorage = processEngineConfiguration.getAnnotationScanner().getExtensionPoint(ExtensionConstant.COMMON,TaskInstanceStorage.class); - + this.taskInstanceStorage = processEngineConfiguration.getAnnotationScanner() + .getExtensionPoint(ExtensionConstant.COMMON, TaskInstanceStorage.class); } - - @Override public void stop() { - + // Clean up resources if needed } @Override public List findPendingTaskList(PendingTaskQueryParam pendingTaskQueryParam) { - return taskInstanceStorage.findPendingTaskList(pendingTaskQueryParam, processEngineConfiguration); } @Override public Long countPendingTaskList(PendingTaskQueryParam pendingTaskQueryParam) { - - - return taskInstanceStorage.countPendingTaskList(pendingTaskQueryParam, processEngineConfiguration); } @@ -61,7 +58,7 @@ public List findTaskListByAssignee(TaskInstanceQueryByAssigneePara @Override public Long countTaskListByAssignee(TaskInstanceQueryByAssigneeParam param) { - return taskInstanceStorage.countTaskListByAssignee(param,processEngineConfiguration ); + return taskInstanceStorage.countTaskListByAssignee(param, processEngineConfiguration); } @Override @@ -70,10 +67,9 @@ public List findAllPendingTaskList(String processInstanceId) { } @Override - public List findAllPendingTaskList(String processInstanceId,String tenantId) { - + public List findAllPendingTaskList(String processInstanceId, String tenantId) { TaskInstanceQueryParam taskInstanceQueryParam = new TaskInstanceQueryParam(); - List processInstanceIdList = new ArrayList(2); + List processInstanceIdList = new ArrayList<>(2); processInstanceIdList.add(processInstanceId); taskInstanceQueryParam.setProcessInstanceIdList(processInstanceIdList); taskInstanceQueryParam.setStatus(TaskInstanceConstant.PENDING); @@ -84,27 +80,78 @@ public List findAllPendingTaskList(String processInstanceId,String @Override public TaskInstance findOne(String taskInstanceId) { - return this.findOne(taskInstanceId,null); + return this.findOne(taskInstanceId, null); } @Override - public TaskInstance findOne(String taskInstanceId,String tenantId) { - TaskInstance taskInstance = taskInstanceStorage.find(taskInstanceId,tenantId, processEngineConfiguration); - return taskInstance; + public TaskInstance findOne(String taskInstanceId, String tenantId) { + return taskInstanceStorage.find(taskInstanceId, tenantId, processEngineConfiguration); } @Override - public List findList(TaskInstanceQueryParam taskInstanceQueryParam){ - + public List findList(TaskInstanceQueryParam taskInstanceQueryParam) { return taskInstanceStorage.findTaskList(taskInstanceQueryParam, processEngineConfiguration); } @Override public Long count(TaskInstanceQueryParam taskInstanceQueryParam) { + return taskInstanceStorage.count(taskInstanceQueryParam, processEngineConfiguration); + } + + @Override + public List findCompletedTaskList(CompletedTaskQueryParam param) { + if (param == null) { + return Collections.emptyList(); + } + + TaskInstanceQueryParam taskInstanceQueryParam = convertToTaskInstanceQueryParam(param); + taskInstanceQueryParam.setStatus(TaskInstanceConstant.COMPLETED); + + return taskInstanceStorage.findTaskList(taskInstanceQueryParam, processEngineConfiguration); + } + + @Override + public Long countCompletedTaskList(CompletedTaskQueryParam param) { + if (param == null) { + return 0L; + } + + TaskInstanceQueryParam taskInstanceQueryParam = convertToTaskInstanceQueryParam(param); + taskInstanceQueryParam.setStatus(TaskInstanceConstant.COMPLETED); return taskInstanceStorage.count(taskInstanceQueryParam, processEngineConfiguration); } + /** + * Convert CompletedTaskQueryParam to TaskInstanceQueryParam. + */ + private TaskInstanceQueryParam convertToTaskInstanceQueryParam(CompletedTaskQueryParam param) { + TaskInstanceQueryParam taskInstanceQueryParam = new TaskInstanceQueryParam(); + + // Pagination + taskInstanceQueryParam.setTenantId(param.getTenantId()); + taskInstanceQueryParam.setPageOffset(param.getPageOffset()); + taskInstanceQueryParam.setPageSize(param.getPageSize()); + + // Query conditions + taskInstanceQueryParam.setClaimUserId(param.getClaimUserId()); + taskInstanceQueryParam.setProcessInstanceIdList(param.getProcessInstanceIdList()); + taskInstanceQueryParam.setTitle(param.getTitle()); + taskInstanceQueryParam.setTag(param.getTag()); + taskInstanceQueryParam.setComment(param.getComment()); + + // Process definition type (take first one if multiple provided) + // TODO: Consider extending TaskInstanceQueryParam to support multiple types + if (param.getProcessDefinitionTypes() != null && !param.getProcessDefinitionTypes().isEmpty()) { + taskInstanceQueryParam.setProcessDefinitionType(param.getProcessDefinitionTypes().get(0)); + } + + // Complete time filter + taskInstanceQueryParam.setCompleteTimeStart(param.getCompleteTimeStart()); + taskInstanceQueryParam.setCompleteTimeEnd(param.getCompleteTimeEnd()); + + return taskInstanceQueryParam; + } @Override public void setProcessEngineConfiguration(ProcessEngineConfiguration processEngineConfiguration) { diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/storage/StorageContext.java b/core/src/main/java/com/alibaba/smart/framework/engine/storage/StorageContext.java new file mode 100644 index 000000000..bfbdde700 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/storage/StorageContext.java @@ -0,0 +1,100 @@ +package com.alibaba.smart.framework.engine.storage; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * Context information for storage operations. + * Can be used to pass request-level storage preferences. + * + * @author SmartEngine Team + */ +public class StorageContext implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * The tenant ID for multi-tenant scenarios + */ + private String tenantId; + + /** + * The storage mode to use for this request + */ + private StorageMode storageMode; + + /** + * Additional context properties + */ + private Map properties; + + private StorageContext() { + this.properties = new HashMap<>(); + } + + public static Builder builder() { + return new Builder(); + } + + public String getTenantId() { + return tenantId; + } + + public StorageMode getStorageMode() { + return storageMode; + } + + public Map getProperties() { + return properties; + } + + public Object getProperty(String key) { + return properties.get(key); + } + + @SuppressWarnings("unchecked") + public T getProperty(String key, Class type) { + Object value = properties.get(key); + if (value == null) { + return null; + } + return (T) value; + } + + public static class Builder { + private final StorageContext context; + + private Builder() { + this.context = new StorageContext(); + } + + public Builder tenantId(String tenantId) { + context.tenantId = tenantId; + return this; + } + + public Builder storageMode(StorageMode storageMode) { + context.storageMode = storageMode; + return this; + } + + public Builder property(String key, Object value) { + context.properties.put(key, value); + return this; + } + + public StorageContext build() { + return context; + } + } + + @Override + public String toString() { + return "StorageContext{" + + "tenantId='" + tenantId + '\'' + + ", storageMode=" + storageMode + + ", properties=" + properties + + '}'; + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/storage/StorageMode.java b/core/src/main/java/com/alibaba/smart/framework/engine/storage/StorageMode.java new file mode 100644 index 000000000..a59b2de20 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/storage/StorageMode.java @@ -0,0 +1,34 @@ +package com.alibaba.smart.framework.engine.storage; + +/** + * Enumeration of storage modes for SmartEngine. + * Supports runtime selection of storage implementation per request. + * + * @author SmartEngine Team + */ +public enum StorageMode { + + /** + * Use database storage (default). + * Data is persisted to relational database. + */ + DATABASE, + + /** + * Use custom storage (e.g. in-memory, custom persistence, etc.). + * Backed by storage-custom module implementations. + */ + CUSTOM, + + /** + * Dual-write mode. + * Writes to both database and memory, reads from memory for performance. + */ + DUAL_WRITE, + + /** + * Read from memory, write to database mode. + * Provides fast reads while ensuring data persistence. + */ + READ_MEMORY_WRITE_DATABASE +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/storage/StorageModeHolder.java b/core/src/main/java/com/alibaba/smart/framework/engine/storage/StorageModeHolder.java new file mode 100644 index 000000000..78e27c890 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/storage/StorageModeHolder.java @@ -0,0 +1,104 @@ +package com.alibaba.smart.framework.engine.storage; + +/** + * ThreadLocal holder for storage mode. + * Allows request-level storage mode selection via ThreadLocal. + * + *

Example usage: + *

{@code
+ * StorageModeHolder.set(StorageMode.DATABASE);
+ * try {
+ *     // All storage operations in this thread will use DATABASE mode
+ *     processService.startProcess(request);
+ * } finally {
+ *     StorageModeHolder.clear();
+ * }
+ * }
+ * + * @author SmartEngine Team + */ +public final class StorageModeHolder { + + private static final ThreadLocal MODE_HOLDER = new ThreadLocal<>(); + private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>(); + + private StorageModeHolder() { + // Utility class, no instantiation + } + + /** + * Set the storage mode for the current thread. + * + * @param mode the storage mode to use + */ + public static void set(StorageMode mode) { + if (mode == null) { + MODE_HOLDER.remove(); + } else { + MODE_HOLDER.set(mode); + } + } + + /** + * Get the storage mode for the current thread. + * + * @return the current storage mode, or null if not set + */ + public static StorageMode get() { + return MODE_HOLDER.get(); + } + + /** + * Get the storage mode for the current thread, or return the default if not set. + * + * @param defaultMode the default mode to return if not set + * @return the current storage mode, or the default if not set + */ + public static StorageMode getOrDefault(StorageMode defaultMode) { + StorageMode mode = MODE_HOLDER.get(); + return mode != null ? mode : defaultMode; + } + + /** + * Clear the storage mode for the current thread. + * Should be called in a finally block to prevent memory leaks. + */ + public static void clear() { + MODE_HOLDER.remove(); + } + + /** + * Set the storage context for the current thread. + * + * @param context the storage context to use + */ + public static void setContext(StorageContext context) { + if (context == null) { + CONTEXT_HOLDER.remove(); + } else { + CONTEXT_HOLDER.set(context); + // Also set the mode from context + if (context.getStorageMode() != null) { + MODE_HOLDER.set(context.getStorageMode()); + } + } + } + + /** + * Get the storage context for the current thread. + * + * @return the current storage context, or null if not set + */ + public static StorageContext getContext() { + return CONTEXT_HOLDER.get(); + } + + /** + * Clear all ThreadLocal values. + * Should be called in a finally block to prevent memory leaks. + */ + public static void clearAll() { + MODE_HOLDER.remove(); + CONTEXT_HOLDER.remove(); + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/storage/StorageRegistry.java b/core/src/main/java/com/alibaba/smart/framework/engine/storage/StorageRegistry.java new file mode 100644 index 000000000..156f939cf --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/storage/StorageRegistry.java @@ -0,0 +1,121 @@ +package com.alibaba.smart.framework.engine.storage; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; + +/** + * Registry for storage implementations by mode. + * Maintains a mapping from StorageMode and storage type to actual implementation. + * + * @author SmartEngine Team + */ +public class StorageRegistry { + + /** + * Storage implementations by mode and type + * Map, Object>> + */ + private final Map, Object>> storageMap; + + public StorageRegistry() { + this.storageMap = new EnumMap<>(StorageMode.class); + for (StorageMode mode : StorageMode.values()) { + storageMap.put(mode, new HashMap<>()); + } + } + + /** + * Register a storage implementation for a specific mode and type. + * + * @param mode the storage mode + * @param storageType the storage interface type + * @param instance the storage implementation instance + * @param the storage type + */ + public void register(StorageMode mode, Class storageType, T instance) { + storageMap.get(mode).put(storageType, instance); + } + + /** + * Get a storage implementation for a specific mode and type. + * + * @param mode the storage mode + * @param storageType the storage interface type + * @param the storage type + * @return the storage implementation, or null if not found + */ + @SuppressWarnings("unchecked") + public T get(StorageMode mode, Class storageType) { + Map, Object> modeMap = storageMap.get(mode); + if (modeMap == null) { + return null; + } + return (T) modeMap.get(storageType); + } + + /** + * Check if a storage implementation is registered for a specific mode and type. + * + * @param mode the storage mode + * @param storageType the storage interface type + * @return true if registered, false otherwise + */ + public boolean contains(StorageMode mode, Class storageType) { + Map, Object> modeMap = storageMap.get(mode); + return modeMap != null && modeMap.containsKey(storageType); + } + + /** + * Remove a storage implementation for a specific mode and type. + * + * @param mode the storage mode + * @param storageType the storage interface type + * @param the storage type + * @return the removed implementation, or null if not found + */ + @SuppressWarnings("unchecked") + public T remove(StorageMode mode, Class storageType) { + Map, Object> modeMap = storageMap.get(mode); + if (modeMap == null) { + return null; + } + return (T) modeMap.remove(storageType); + } + + /** + * Clear all storage implementations. + */ + public void clear() { + for (Map, Object> modeMap : storageMap.values()) { + modeMap.clear(); + } + } + + /** + * Clear all storage implementations for a specific mode. + * + * @param mode the storage mode + */ + public void clear(StorageMode mode) { + Map, Object> modeMap = storageMap.get(mode); + if (modeMap != null) { + modeMap.clear(); + } + } + + /** + * Check if a storage type is registered under any mode. + * + * @param storageType the storage interface type + * @return true if registered under at least one mode + */ + public boolean containsType(Class storageType) { + for (Map, Object> modeMap : storageMap.values()) { + if (modeMap.containsKey(storageType)) { + return true; + } + } + return false; + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/storage/StorageRouter.java b/core/src/main/java/com/alibaba/smart/framework/engine/storage/StorageRouter.java new file mode 100644 index 000000000..00ad49abf --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/storage/StorageRouter.java @@ -0,0 +1,174 @@ +package com.alibaba.smart.framework.engine.storage; + +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; + +/** + * Router for selecting storage implementations based on current mode. + * Supports request-level storage mode selection via ThreadLocal or context. + * + *

The router resolves storage in the following order: + *

    + *
  1. Check ThreadLocal (StorageModeHolder) for request-level override
  2. + *
  3. Check StorageContext if provided
  4. + *
  5. Use default mode from configuration
  6. + *
  7. Fall back to DATABASE mode
  8. + *
+ * + *

Example usage: + *

{@code
+ * // Configure router in ProcessEngineConfiguration
+ * StorageRouter router = new StorageRouter(config);
+ *
+ * // Get storage using current mode (from ThreadLocal or default)
+ * TaskInstanceStorage storage = router.getStorage(TaskInstanceStorage.class);
+ *
+ * // Get storage for specific mode
+ * TaskInstanceStorage customStorage = router.getStorage(TaskInstanceStorage.class, StorageMode.CUSTOM);
+ * }
+ * + * @author SmartEngine Team + */ +public class StorageRouter { + + private final ProcessEngineConfiguration configuration; + private final StorageRegistry registry; + private final List strategies; + private StorageMode defaultMode = StorageMode.DATABASE; + + public StorageRouter(ProcessEngineConfiguration configuration) { + this.configuration = configuration; + this.registry = new StorageRegistry(); + this.strategies = new CopyOnWriteArrayList<>(); + } + + /** + * Get a storage implementation using the current mode. + * Mode is resolved from ThreadLocal, then configuration default. + * + * @param storageType the storage interface type + * @param the storage type + * @return the storage implementation + */ + public T getStorage(Class storageType) { + StorageMode mode = resolveCurrentMode(); + return getStorage(storageType, mode); + } + + /** + * Get a storage implementation for a specific mode. + * + * @param storageType the storage interface type + * @param mode the storage mode + * @param the storage type + * @return the storage implementation + */ + public T getStorage(Class storageType, StorageMode mode) { + // First check registry + T storage = registry.get(mode, storageType); + if (storage != null) { + return storage; + } + + // Try strategies + StorageContext context = StorageModeHolder.getContext(); + for (StorageStrategy strategy : strategies) { + if (strategy.getStorageMode() == mode && strategy.supportsStorageType(storageType)) { + storage = strategy.resolveStorage(storageType, context, configuration); + if (storage != null) { + return storage; + } + } + } + + // No implementation found in registry or strategies + throw new IllegalStateException( + "No storage implementation found for type " + storageType.getName() + " and mode " + mode); + } + + /** + * Resolve the current storage mode. + * Checks ThreadLocal first, then context, then default. + * + * @return the resolved storage mode + */ + public StorageMode resolveCurrentMode() { + // 1. Check ThreadLocal + StorageMode mode = StorageModeHolder.get(); + if (mode != null) { + return mode; + } + + // 2. Check context + StorageContext context = StorageModeHolder.getContext(); + if (context != null && context.getStorageMode() != null) { + return context.getStorageMode(); + } + + // 3. Use default + return defaultMode; + } + + /** + * Register a storage implementation. + * + * @param mode the storage mode + * @param storageType the storage interface type + * @param instance the storage implementation + * @param the storage type + */ + public void registerStorage(StorageMode mode, Class storageType, T instance) { + registry.register(mode, storageType, instance); + } + + /** + * Register a storage strategy. + * + * @param strategy the strategy to register + */ + public void registerStrategy(StorageStrategy strategy) { + strategies.add(strategy); + // Sort by priority (descending) + strategies.sort(Comparator.comparingInt(StorageStrategy::getPriority).reversed()); + } + + /** + * Set the default storage mode. + * + * @param defaultMode the default mode + */ + public void setDefaultMode(StorageMode defaultMode) { + this.defaultMode = defaultMode; + } + + /** + * Get the default storage mode. + * + * @return the default mode + */ + public StorageMode getDefaultMode() { + return defaultMode; + } + + /** + * Get the storage registry. + * + * @return the registry + */ + public StorageRegistry getRegistry() { + return registry; + } + + /** + * Check if a storage type is registered in the router under any mode. + * + * @param storageType the storage interface type + * @return true if registered + */ + public boolean hasStorageType(Class storageType) { + return registry.containsType(storageType); + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/storage/StorageStrategy.java b/core/src/main/java/com/alibaba/smart/framework/engine/storage/StorageStrategy.java new file mode 100644 index 000000000..50162a3e7 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/storage/StorageStrategy.java @@ -0,0 +1,49 @@ +package com.alibaba.smart.framework.engine.storage; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; + +/** + * Strategy interface for resolving storage implementations. + * Implementations can provide different storage resolution logic + * based on storage mode, context, or other factors. + * + * @author SmartEngine Team + */ +public interface StorageStrategy { + + /** + * Get the storage mode this strategy handles. + * + * @return the storage mode + */ + StorageMode getStorageMode(); + + /** + * Resolve a storage implementation for the given type. + * + * @param storageType the storage interface type + * @param context the storage context (may be null) + * @param config the engine configuration + * @param the storage type + * @return the storage implementation + */ + T resolveStorage(Class storageType, StorageContext context, ProcessEngineConfiguration config); + + /** + * Check if this strategy supports the given storage type. + * + * @param storageType the storage interface type + * @return true if supported, false otherwise + */ + boolean supportsStorageType(Class storageType); + + /** + * Get the priority of this strategy (higher = more preferred). + * Used when multiple strategies could handle the same request. + * + * @return the priority value + */ + default int getPriority() { + return 0; + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/storage/strategy/CustomStorageStrategy.java b/core/src/main/java/com/alibaba/smart/framework/engine/storage/strategy/CustomStorageStrategy.java new file mode 100644 index 000000000..3b3546bbd --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/storage/strategy/CustomStorageStrategy.java @@ -0,0 +1,71 @@ +package com.alibaba.smart.framework.engine.storage.strategy; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.storage.StorageContext; +import com.alibaba.smart.framework.engine.storage.StorageMode; +import com.alibaba.smart.framework.engine.storage.StorageStrategy; + +/** + * Storage strategy for custom mode. + * Uses pre-registered custom storage implementations. + * + * @author SmartEngine Team + */ +public class CustomStorageStrategy implements StorageStrategy { + + private final Map, Object> customStorages = new ConcurrentHashMap<>(); + + @Override + public StorageMode getStorageMode() { + return StorageMode.CUSTOM; + } + + @Override + @SuppressWarnings("unchecked") + public T resolveStorage(Class storageType, StorageContext context, ProcessEngineConfiguration config) { + return (T) customStorages.get(storageType); + } + + @Override + public boolean supportsStorageType(Class storageType) { + return customStorages.containsKey(storageType); + } + + /** + * Register a custom storage implementation. + * + * @param storageType the storage interface type + * @param instance the custom storage implementation + * @param the storage type + */ + public void registerCustomStorage(Class storageType, T instance) { + customStorages.put(storageType, instance); + } + + /** + * Remove a custom storage implementation. + * + * @param storageType the storage interface type + * @param the storage type + * @return the removed implementation + */ + @SuppressWarnings("unchecked") + public T removeCustomStorage(Class storageType) { + return (T) customStorages.remove(storageType); + } + + /** + * Clear all custom storage implementations. + */ + public void clear() { + customStorages.clear(); + } + + @Override + public int getPriority() { + return 10; // Higher priority than database for custom mode + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/storage/strategy/DatabaseStorageStrategy.java b/core/src/main/java/com/alibaba/smart/framework/engine/storage/strategy/DatabaseStorageStrategy.java new file mode 100644 index 000000000..8f90a4349 --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/storage/strategy/DatabaseStorageStrategy.java @@ -0,0 +1,43 @@ +package com.alibaba.smart.framework.engine.storage.strategy; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.scanner.AnnotationScanner; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.storage.StorageContext; +import com.alibaba.smart.framework.engine.storage.StorageMode; +import com.alibaba.smart.framework.engine.storage.StorageStrategy; + +/** + * Storage strategy for database mode. + * Resolves storage implementations from AnnotationScanner (existing mechanism). + * + * @author SmartEngine Team + */ +public class DatabaseStorageStrategy implements StorageStrategy { + + @Override + public StorageMode getStorageMode() { + return StorageMode.DATABASE; + } + + @Override + public T resolveStorage(Class storageType, StorageContext context, ProcessEngineConfiguration config) { + AnnotationScanner scanner = config.getAnnotationScanner(); + if (scanner != null) { + return scanner.getExtensionPoint(ExtensionConstant.COMMON, storageType); + } + return null; + } + + @Override + public boolean supportsStorageType(Class storageType) { + // Database strategy supports all storage types + return true; + } + + @Override + public int getPriority() { + // Default priority + return 0; + } +} diff --git a/core/src/main/java/com/alibaba/smart/framework/engine/storage/strategy/DualStorageStrategy.java b/core/src/main/java/com/alibaba/smart/framework/engine/storage/strategy/DualStorageStrategy.java new file mode 100644 index 000000000..30e07b44c --- /dev/null +++ b/core/src/main/java/com/alibaba/smart/framework/engine/storage/strategy/DualStorageStrategy.java @@ -0,0 +1,147 @@ +package com.alibaba.smart.framework.engine.storage.strategy; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.storage.StorageContext; +import com.alibaba.smart.framework.engine.storage.StorageMode; +import com.alibaba.smart.framework.engine.storage.StorageStrategy; + +/** + * Storage strategy for dual-write mode. + * Writes to both database and custom storage, reads from custom storage for better performance. + * + * @author SmartEngine Team + */ +public class DualStorageStrategy implements StorageStrategy { + + private final DatabaseStorageStrategy databaseStrategy; + private final CustomStorageStrategy customStrategy; + private final Map, Object> proxyCache = new ConcurrentHashMap<>(); + + /** + * Method name prefixes that indicate write operations. + */ + private static final Set WRITE_PREFIXES = new HashSet<>(Arrays.asList( + "insert", "update", "delete", "remove", "save", "create", "add", "close", "mark", "batch" + )); + + public DualStorageStrategy(DatabaseStorageStrategy databaseStrategy, CustomStorageStrategy customStrategy) { + this.databaseStrategy = databaseStrategy; + this.customStrategy = customStrategy; + } + + @Override + public StorageMode getStorageMode() { + return StorageMode.DUAL_WRITE; + } + + @Override + @SuppressWarnings("unchecked") + public T resolveStorage(Class storageType, StorageContext context, ProcessEngineConfiguration config) { + return (T) proxyCache.computeIfAbsent(storageType, type -> createProxy(type, context, config)); + } + + @SuppressWarnings("unchecked") + private T createProxy(Class storageType, StorageContext context, ProcessEngineConfiguration config) { + T databaseStorage = databaseStrategy.resolveStorage(storageType, context, config); + T customStorage = customStrategy.resolveStorage(storageType, context, config); + + if (databaseStorage == null) { + throw new IllegalStateException("Database storage not found for type: " + storageType.getName()); + } + + // If no custom storage is registered, just return database storage + if (customStorage == null) { + return databaseStorage; + } + + return (T) Proxy.newProxyInstance( + storageType.getClassLoader(), + new Class[]{storageType}, + new DualStorageInvocationHandler(databaseStorage, customStorage) + ); + } + + @Override + public boolean supportsStorageType(Class storageType) { + return databaseStrategy.supportsStorageType(storageType); + } + + @Override + public int getPriority() { + return 20; // Higher priority than both database and memory + } + + /** + * Invocation handler for dual-write proxy. + * Routes write operations to both storages, read operations to custom storage first. + */ + private static class DualStorageInvocationHandler implements InvocationHandler { + + private final Object databaseStorage; + private final Object customStorage; + + DualStorageInvocationHandler(Object databaseStorage, Object customStorage) { + this.databaseStorage = databaseStorage; + this.customStorage = customStorage; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String methodName = method.getName(); + + if (isWriteOperation(methodName)) { + // Write to database first (source of truth) + Object result = method.invoke(databaseStorage, args); + + // Then sync to memory (best effort) + try { + method.invoke(customStorage, args); + } catch (Exception e) { + // Log but don't fail the operation + // In production, this should use proper logging + System.err.println("Failed to sync write to custom storage: " + e.getMessage()); + } + + return result; + } else { + // Read from memory first, fall back to database + try { + Object result = method.invoke(customStorage, args); + if (result != null) { + return result; + } + } catch (Exception e) { + // Fall through to database + } + return method.invoke(databaseStorage, args); + } + } + + private boolean isWriteOperation(String methodName) { + String lowerName = methodName.toLowerCase(); + for (String prefix : WRITE_PREFIXES) { + if (lowerName.startsWith(prefix)) { + return true; + } + } + return false; + } + } + + /** + * Clear the proxy cache. + * Useful when storage configurations change. + */ + public void clearCache() { + proxyCache.clear(); + } +} diff --git a/core/src/test/java/com/alibaba/smart/framework/engine/configuration/impl/SnowflakeIdGeneratorTest.java b/core/src/test/java/com/alibaba/smart/framework/engine/configuration/impl/SnowflakeIdGeneratorTest.java new file mode 100644 index 000000000..87db638c3 --- /dev/null +++ b/core/src/test/java/com/alibaba/smart/framework/engine/configuration/impl/SnowflakeIdGeneratorTest.java @@ -0,0 +1,202 @@ +package com.alibaba.smart.framework.engine.configuration.impl; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for SnowflakeIdGenerator. + */ +public class SnowflakeIdGeneratorTest { + + @Test + public void testBasicGeneration() { + SnowflakeIdGenerator generator = new SnowflakeIdGenerator(0); + long id = generator.nextId(); + Assert.assertTrue("ID should be positive", id > 0); + } + + @Test + public void testMonotonicallyIncreasing() { + SnowflakeIdGenerator generator = new SnowflakeIdGenerator(0); + long prev = 0; + for (int i = 0; i < 10000; i++) { + long id = generator.nextId(); + Assert.assertTrue("IDs should be monotonically increasing: prev=" + prev + " current=" + id, id > prev); + prev = id; + } + } + + @Test + public void testUniqueness() { + SnowflakeIdGenerator generator = new SnowflakeIdGenerator(0); + Set ids = new HashSet(); + int count = 100000; + for (int i = 0; i < count; i++) { + long id = generator.nextId(); + Assert.assertTrue("Duplicate ID detected: " + id, ids.add(id)); + } + Assert.assertEquals(count, ids.size()); + } + + @Test + public void testParseTimestamp() { + SnowflakeIdGenerator generator = new SnowflakeIdGenerator(0); + long beforeMs = System.currentTimeMillis(); + long id = generator.nextId(); + long afterMs = System.currentTimeMillis(); + + long parsedTimestamp = SnowflakeIdGenerator.parseTimestamp(id); + Assert.assertTrue("Parsed timestamp should be >= beforeMs", + parsedTimestamp >= beforeMs); + Assert.assertTrue("Parsed timestamp should be <= afterMs", + parsedTimestamp <= afterMs); + } + + @Test + public void testParseNodeId() { + for (int nodeId = 0; nodeId < 32; nodeId++) { + SnowflakeIdGenerator generator = new SnowflakeIdGenerator(nodeId); + long id = generator.nextId(); + Assert.assertEquals("Node ID should match", nodeId, SnowflakeIdGenerator.parseNodeId(id)); + } + } + + @Test + public void testParseSequence() { + SnowflakeIdGenerator generator = new SnowflakeIdGenerator(0); + long id = generator.nextId(); + long seq = SnowflakeIdGenerator.parseSequence(id); + Assert.assertTrue("Sequence should be >= 0", seq >= 0); + Assert.assertTrue("Sequence should be <= 131071", seq <= 131071); + } + + @Test + public void testDifferentNodesProduceDifferentIds() { + SnowflakeIdGenerator gen0 = new SnowflakeIdGenerator(0); + SnowflakeIdGenerator gen1 = new SnowflakeIdGenerator(1); + + long id0 = gen0.nextId(); + long id1 = gen1.nextId(); + + Assert.assertNotEquals("Different nodes should produce different IDs", id0, id1); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidNodeIdNegative() { + new SnowflakeIdGenerator(-1); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidNodeIdTooLarge() { + new SnowflakeIdGenerator(32); + } + + @Test + public void testDefaultConstructor() { + SnowflakeIdGenerator generator = new SnowflakeIdGenerator(); + long id = generator.nextId(); + Assert.assertEquals("Default node ID should be 0", 0, SnowflakeIdGenerator.parseNodeId(id)); + } + + @Test + public void testGetIdType() { + SnowflakeIdGenerator generator = new SnowflakeIdGenerator(); + Assert.assertEquals(Long.class, generator.getIdType()); + } + + @Test + public void testConcurrentUniqueness() throws InterruptedException { + final SnowflakeIdGenerator generator = new SnowflakeIdGenerator(0); + final int threadCount = 8; + final int idsPerThread = 10000; + final Set allIds = ConcurrentHashMap.newKeySet(); + final AtomicBoolean hasDuplicate = new AtomicBoolean(false); + final CountDownLatch latch = new CountDownLatch(threadCount); + + for (int t = 0; t < threadCount; t++) { + new Thread(new Runnable() { + @Override + public void run() { + try { + for (int i = 0; i < idsPerThread; i++) { + long id = generator.nextId(); + if (!allIds.add(id)) { + hasDuplicate.set(true); + } + } + } finally { + latch.countDown(); + } + } + }).start(); + } + + latch.await(); + Assert.assertFalse("No duplicate IDs should exist across threads", hasDuplicate.get()); + Assert.assertEquals("Total unique IDs should match", + threadCount * idsPerThread, allIds.size()); + } + + @Test + public void testSequenceWraparound() { + // Generate many IDs in quick succession to trigger sequence increment + SnowflakeIdGenerator generator = new SnowflakeIdGenerator(0); + long prev = 0; + for (int i = 0; i < 200000; i++) { + long id = generator.nextId(); + Assert.assertTrue("IDs should remain monotonically increasing during sequence wrap", + id > prev); + prev = id; + } + } + + @Test + public void testClockRollbackSmall() { + // Create a generator that simulates 3ms clock rollback then recovers + SnowflakeIdGenerator generator = new SnowflakeIdGenerator(0) { + private int callCount = 0; + @Override + long currentTimeMillis() { + callCount++; + if (callCount == 2) { + // Second call simulates 3ms rollback + return System.currentTimeMillis() - 3; + } + return System.currentTimeMillis(); + } + }; + + // First call establishes lastTimestamp + long id1 = generator.nextId(); + // Second call triggers rollback handling (should wait and recover) + long id2 = generator.nextId(); + + Assert.assertTrue("Should still generate valid IDs after small rollback", id2 > 0); + } + + @Test(expected = IllegalStateException.class) + public void testClockRollbackLarge() { + // Create a generator that simulates large clock rollback + SnowflakeIdGenerator generator = new SnowflakeIdGenerator(0) { + private int callCount = 0; + @Override + long currentTimeMillis() { + callCount++; + if (callCount >= 2) { + // All subsequent calls return a time 10ms in the past + return super.currentTimeMillis() - 100; + } + return super.currentTimeMillis(); + } + }; + + generator.nextId(); // Establishes lastTimestamp + generator.nextId(); // Should throw IllegalStateException + } +} diff --git a/core/src/test/java/com/alibaba/smart/framework/engine/dialect/DialectRegistryTest.java b/core/src/test/java/com/alibaba/smart/framework/engine/dialect/DialectRegistryTest.java new file mode 100644 index 000000000..a62baddc2 --- /dev/null +++ b/core/src/test/java/com/alibaba/smart/framework/engine/dialect/DialectRegistryTest.java @@ -0,0 +1,133 @@ +package com.alibaba.smart.framework.engine.dialect; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for DialectRegistry. + * + * @author SmartEngine Team + */ +public class DialectRegistryTest { + + @Test + public void testGetInstance() { + DialectRegistry registry = DialectRegistry.getInstance(); + Assert.assertNotNull(registry); + + // Same instance + Assert.assertSame(registry, DialectRegistry.getInstance()); + } + + @Test + public void testBuiltInDialects() { + DialectRegistry registry = DialectRegistry.getInstance(); + + // Verify all built-in dialects are registered + Assert.assertTrue(registry.hasDialect("mysql")); + Assert.assertTrue(registry.hasDialect("postgresql")); + Assert.assertTrue(registry.hasDialect("h2")); + Assert.assertTrue(registry.hasDialect("oracle")); + Assert.assertTrue(registry.hasDialect("sqlserver")); + Assert.assertTrue(registry.hasDialect("dm")); + Assert.assertTrue(registry.hasDialect("kingbase")); + Assert.assertTrue(registry.hasDialect("oceanbase")); + } + + @Test + public void testGetDialectCaseInsensitive() { + DialectRegistry registry = DialectRegistry.getInstance(); + + Dialect mysql1 = registry.getDialect("mysql"); + Dialect mysql2 = registry.getDialect("MYSQL"); + Dialect mysql3 = registry.getDialect("MySQL"); + + Assert.assertNotNull(mysql1); + Assert.assertSame(mysql1, mysql2); + Assert.assertSame(mysql1, mysql3); + } + + @Test + public void testGetDialectOrThrow() { + DialectRegistry registry = DialectRegistry.getInstance(); + + Dialect dialect = registry.getDialectOrThrow("postgresql"); + Assert.assertNotNull(dialect); + Assert.assertEquals("postgresql", dialect.getName()); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetDialectOrThrowNotFound() { + DialectRegistry registry = DialectRegistry.getInstance(); + registry.getDialectOrThrow("nonexistent"); + } + + @Test + public void testDetectDialectFromJdbcUrl() { + DialectRegistry registry = DialectRegistry.getInstance(); + + // MySQL + Dialect mysql = registry.detectDialect("jdbc:mysql://localhost:3306/test"); + Assert.assertNotNull(mysql); + Assert.assertEquals("mysql", mysql.getName()); + + // PostgreSQL + Dialect pg = registry.detectDialect("jdbc:postgresql://localhost:5432/test"); + Assert.assertNotNull(pg); + Assert.assertEquals("postgresql", pg.getName()); + + // H2 + Dialect h2 = registry.detectDialect("jdbc:h2:mem:test"); + Assert.assertNotNull(h2); + Assert.assertEquals("h2", h2.getName()); + + // Oracle + Dialect oracle = registry.detectDialect("jdbc:oracle:thin:@localhost:1521:orcl"); + Assert.assertNotNull(oracle); + Assert.assertEquals("oracle", oracle.getName()); + + // SQL Server + Dialect sqlserver = registry.detectDialect("jdbc:sqlserver://localhost:1433;databaseName=test"); + Assert.assertNotNull(sqlserver); + Assert.assertEquals("sqlserver", sqlserver.getName()); + + // DM (达梦) + Dialect dm = registry.detectDialect("jdbc:dm://localhost:5236/test"); + Assert.assertNotNull(dm); + Assert.assertEquals("dm", dm.getName()); + + // Kingbase (人大金仓) + Dialect kingbase = registry.detectDialect("jdbc:kingbase://localhost:54321/test"); + Assert.assertNotNull(kingbase); + Assert.assertEquals("kingbase", kingbase.getName()); + + // OceanBase + Dialect oceanbase = registry.detectDialect("jdbc:oceanbase://localhost:2881/test"); + Assert.assertNotNull(oceanbase); + Assert.assertEquals("oceanbase", oceanbase.getName()); + } + + @Test + public void testDetectDialectUnknown() { + DialectRegistry registry = DialectRegistry.getInstance(); + + Dialect unknown = registry.detectDialect("jdbc:unknown://localhost/test"); + Assert.assertNull(unknown); + } + + @Test + public void testGetAllDialects() { + DialectRegistry registry = DialectRegistry.getInstance(); + + Assert.assertTrue(registry.getAllDialects().size() >= 8); + } + + @Test + public void testDefaultDialect() { + DialectRegistry registry = DialectRegistry.getInstance(); + + Dialect defaultDialect = registry.getDefaultDialect(); + Assert.assertNotNull(defaultDialect); + Assert.assertEquals("mysql", defaultDialect.getName()); + } +} diff --git a/core/src/test/java/com/alibaba/smart/framework/engine/dialect/DialectTest.java b/core/src/test/java/com/alibaba/smart/framework/engine/dialect/DialectTest.java new file mode 100644 index 000000000..4d36e1886 --- /dev/null +++ b/core/src/test/java/com/alibaba/smart/framework/engine/dialect/DialectTest.java @@ -0,0 +1,382 @@ +package com.alibaba.smart.framework.engine.dialect; + +import com.alibaba.smart.framework.engine.dialect.impl.*; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for Dialect implementations. + * + * @author SmartEngine Team + */ +public class DialectTest { + + // ============ MySQL Dialect Tests ============ + + @Test + public void testMySqlDialect() { + MySqlDialect dialect = new MySqlDialect(); + + Assert.assertEquals("mysql", dialect.getName()); + Assert.assertEquals("MySQL", dialect.getDisplayName()); + Assert.assertTrue(dialect.supportsLimitOffset()); + Assert.assertEquals(IdGenerationType.AUTO_INCREMENT, dialect.getIdGenerationType()); + } + + @Test + public void testMySqlBuildPageSql() { + MySqlDialect dialect = new MySqlDialect(); + + String sql = "SELECT * FROM users WHERE status = 'active'"; + String pageSql = dialect.buildPageSql(sql, 10, 20); + + Assert.assertEquals("SELECT * FROM users WHERE status = 'active' LIMIT 20 OFFSET 10", pageSql); + } + + @Test + public void testMySqlQuoteIdentifier() { + MySqlDialect dialect = new MySqlDialect(); + + Assert.assertEquals("`table_name`", dialect.quoteIdentifier("table_name")); + } + + // ============ PostgreSQL Dialect Tests ============ + + @Test + public void testPostgreSqlDialect() { + PostgreSqlDialect dialect = new PostgreSqlDialect(); + + Assert.assertEquals("postgresql", dialect.getName()); + Assert.assertEquals("PostgreSQL", dialect.getDisplayName()); + Assert.assertTrue(dialect.supportsLimitOffset()); + Assert.assertEquals(IdGenerationType.IDENTITY, dialect.getIdGenerationType()); + } + + @Test + public void testPostgreSqlBuildPageSql() { + PostgreSqlDialect dialect = new PostgreSqlDialect(); + + String sql = "SELECT * FROM users"; + String pageSql = dialect.buildPageSql(sql, 0, 10); + + Assert.assertEquals("SELECT * FROM users LIMIT 10 OFFSET 0", pageSql); + } + + @Test + public void testPostgreSqlConcat() { + PostgreSqlDialect dialect = new PostgreSqlDialect(); + + String concat = dialect.concat("first_name", "' '", "last_name"); + Assert.assertEquals("(first_name || ' ' || last_name)", concat); + } + + @Test + public void testPostgreSqlSequence() { + PostgreSqlDialect dialect = new PostgreSqlDialect(); + + String nextVal = dialect.getSequenceNextValueSql("user_id_seq"); + Assert.assertEquals("nextval('user_id_seq')", nextVal); + } + + // ============ H2 Dialect Tests ============ + + @Test + public void testH2Dialect() { + H2Dialect dialect = new H2Dialect(); + + Assert.assertEquals("h2", dialect.getName()); + Assert.assertEquals("H2 Database", dialect.getDisplayName()); + Assert.assertTrue(dialect.supportsLimitOffset()); + Assert.assertEquals(IdGenerationType.AUTO_INCREMENT, dialect.getIdGenerationType()); + } + + @Test + public void testH2DateDiff() { + H2Dialect dialect = new H2Dialect(); + + String dateDiff = dialect.getDateDiff("start_date", "end_date"); + Assert.assertEquals("DATEDIFF('DAY', start_date, end_date)", dateDiff); + } + + // ============ Oracle Dialect Tests ============ + + @Test + public void testOracleDialect() { + OracleDialect dialect = new OracleDialect(); + + Assert.assertEquals("oracle", dialect.getName()); + Assert.assertEquals("Oracle Database", dialect.getDisplayName()); + Assert.assertEquals(IdGenerationType.SEQUENCE, dialect.getIdGenerationType()); + } + + @Test + public void testOracleBuildPageSql() { + OracleDialect dialect = new OracleDialect(); + + String sql = "SELECT * FROM users ORDER BY id"; + String pageSql = dialect.buildPageSql(sql, 10, 20); + + Assert.assertEquals("SELECT * FROM users ORDER BY id OFFSET 10 ROWS FETCH NEXT 20 ROWS ONLY", pageSql); + } + + @Test + public void testOracleBuildPageSqlLegacy() { + OracleDialect dialect = new OracleDialect(); + + String sql = "SELECT * FROM users ORDER BY id"; + String pageSql = dialect.buildPageSqlLegacy(sql, 10, 20); + + Assert.assertTrue(pageSql.contains("ROWNUM")); + Assert.assertTrue(pageSql.contains("rn > 10")); + } + + @Test + public void testOracleVarcharType() { + OracleDialect dialect = new OracleDialect(); + + Assert.assertEquals("VARCHAR2(100)", dialect.getVarcharType(100)); + } + + @Test + public void testOracleSequence() { + OracleDialect dialect = new OracleDialect(); + + String nextVal = dialect.getSequenceNextValueSql("user_id_seq"); + Assert.assertEquals("user_id_seq.NEXTVAL", nextVal); + } + + // ============ SQL Server Dialect Tests ============ + + @Test + public void testSqlServerDialect() { + SqlServerDialect dialect = new SqlServerDialect(); + + Assert.assertEquals("sqlserver", dialect.getName()); + Assert.assertEquals("Microsoft SQL Server", dialect.getDisplayName()); + Assert.assertEquals(IdGenerationType.IDENTITY, dialect.getIdGenerationType()); + } + + @Test + public void testSqlServerBuildPageSql() { + SqlServerDialect dialect = new SqlServerDialect(); + + String sql = "SELECT * FROM users ORDER BY id"; + String pageSql = dialect.buildPageSql(sql, 10, 20); + + Assert.assertEquals("SELECT * FROM users ORDER BY id OFFSET 10 ROWS FETCH NEXT 20 ROWS ONLY", pageSql); + } + + @Test + public void testSqlServerVarcharType() { + SqlServerDialect dialect = new SqlServerDialect(); + + Assert.assertEquals("NVARCHAR(255)", dialect.getVarcharType(255)); + } + + @Test + public void testSqlServerQuoteIdentifier() { + SqlServerDialect dialect = new SqlServerDialect(); + + Assert.assertEquals("[table_name]", dialect.quoteIdentifier("table_name")); + } + + // ============ DM (达梦) Dialect Tests ============ + + @Test + public void testDmDialect() { + DmDialect dialect = new DmDialect(); + + Assert.assertEquals("dm", dialect.getName()); + Assert.assertEquals("达梦数据库 (DM)", dialect.getDisplayName()); + Assert.assertTrue(dialect.supportsLimitOffset()); + Assert.assertEquals(IdGenerationType.IDENTITY, dialect.getIdGenerationType()); + } + + @Test + public void testDmBuildPageSql() { + DmDialect dialect = new DmDialect(); + + String sql = "SELECT * FROM users"; + String pageSql = dialect.buildPageSql(sql, 5, 15); + + Assert.assertEquals("SELECT * FROM users LIMIT 15 OFFSET 5", pageSql); + } + + // ============ Kingbase (人大金仓) Dialect Tests ============ + + @Test + public void testKingbaseDialect() { + KingbaseDialect dialect = new KingbaseDialect(); + + Assert.assertEquals("kingbase", dialect.getName()); + Assert.assertEquals("人大金仓 (KingbaseES)", dialect.getDisplayName()); + Assert.assertTrue(dialect.supportsLimitOffset()); + Assert.assertEquals(IdGenerationType.IDENTITY, dialect.getIdGenerationType()); + } + + @Test + public void testKingbaseConcat() { + KingbaseDialect dialect = new KingbaseDialect(); + + // PostgreSQL compatible + String concat = dialect.concat("a", "b", "c"); + Assert.assertEquals("(a || b || c)", concat); + } + + // ============ OceanBase Dialect Tests ============ + + @Test + public void testOceanBaseDialect() { + OceanBaseDialect dialect = new OceanBaseDialect(); + + Assert.assertEquals("oceanbase", dialect.getName()); + Assert.assertEquals("OceanBase", dialect.getDisplayName()); + Assert.assertTrue(dialect.supportsLimitOffset()); + Assert.assertEquals(IdGenerationType.AUTO_INCREMENT, dialect.getIdGenerationType()); + } + + @Test + public void testOceanBaseQuoteIdentifier() { + OceanBaseDialect dialect = new OceanBaseDialect(); + + // MySQL compatible + Assert.assertEquals("`table_name`", dialect.quoteIdentifier("table_name")); + } + + // ============ Feature Support Tests ============ + + @Test + public void testDialectFeatureSupport() { + MySqlDialect mysql = new MySqlDialect(); + PostgreSqlDialect pg = new PostgreSqlDialect(); + OracleDialect oracle = new OracleDialect(); + + // LIMIT_OFFSET + Assert.assertTrue(mysql.supportsFeature(Dialect.DialectFeature.LIMIT_OFFSET)); + Assert.assertTrue(pg.supportsFeature(Dialect.DialectFeature.LIMIT_OFFSET)); + Assert.assertTrue(oracle.supportsFeature(Dialect.DialectFeature.LIMIT_OFFSET)); + + // SEQUENCES + Assert.assertFalse(mysql.supportsFeature(Dialect.DialectFeature.SEQUENCES)); + Assert.assertTrue(pg.supportsFeature(Dialect.DialectFeature.SEQUENCES)); + Assert.assertTrue(oracle.supportsFeature(Dialect.DialectFeature.SEQUENCES)); + + // ARRAY_TYPE + Assert.assertFalse(mysql.supportsFeature(Dialect.DialectFeature.ARRAY_TYPE)); + Assert.assertTrue(pg.supportsFeature(Dialect.DialectFeature.ARRAY_TYPE)); + } + + // ============ Common Functions Tests ============ + + @Test + public void testCurrentTimestamp() { + Assert.assertEquals("CURRENT_TIMESTAMP", new MySqlDialect().getCurrentTimestamp()); + Assert.assertEquals("CURRENT_TIMESTAMP", new PostgreSqlDialect().getCurrentTimestamp()); + Assert.assertEquals("SYSTIMESTAMP", new OracleDialect().getCurrentTimestamp()); + Assert.assertEquals("GETDATE()", new SqlServerDialect().getCurrentTimestamp()); + } + + @Test + public void testClobType() { + Assert.assertEquals("TEXT", new MySqlDialect().getClobType()); + Assert.assertEquals("TEXT", new PostgreSqlDialect().getClobType()); + Assert.assertEquals("CLOB", new OracleDialect().getClobType()); + Assert.assertEquals("NVARCHAR(MAX)", new SqlServerDialect().getClobType()); + Assert.assertEquals("CLOB", new H2Dialect().getClobType()); + } + + @Test + public void testForUpdateClause() { + MySqlDialect dialect = new MySqlDialect(); + + Assert.assertEquals("FOR UPDATE", dialect.getForUpdateClause()); + Assert.assertEquals("FOR UPDATE NOWAIT", dialect.getForUpdateNoWaitClause()); + } + + @Test + public void testBooleanLiterals() { + MySqlDialect dialect = new MySqlDialect(); + + Assert.assertEquals("TRUE", dialect.getBooleanTrue()); + Assert.assertEquals("FALSE", dialect.getBooleanFalse()); + } + + @Test + public void testSubstring() { + MySqlDialect mysql = new MySqlDialect(); + OracleDialect oracle = new OracleDialect(); + + Assert.assertEquals("SUBSTRING(name, 1, 10)", mysql.substring("name", 1, 10)); + Assert.assertEquals("SUBSTR(name, 1, 10)", oracle.substring("name", 1, 10)); + } + + // ============ JSON Extract Text Tests ============ + + @Test + public void testMySqlJsonExtractText() { + MySqlDialect dialect = new MySqlDialect(); + String result = dialect.jsonExtractText("task.extra", "category"); + Assert.assertEquals("JSON_UNQUOTE(JSON_EXTRACT(task.extra, '$.category'))", result); + } + + @Test + public void testPostgreSqlJsonExtractText() { + PostgreSqlDialect dialect = new PostgreSqlDialect(); + String result = dialect.jsonExtractText("task.extra", "category"); + Assert.assertEquals("(task.extra->>'category')", result); + } + + @Test + public void testOracleJsonExtractText() { + OracleDialect dialect = new OracleDialect(); + String result = dialect.jsonExtractText("task.extra", "category"); + Assert.assertEquals("JSON_VALUE(task.extra, '$.category')", result); + } + + @Test + public void testH2JsonExtractText() { + H2Dialect dialect = new H2Dialect(); + String result = dialect.jsonExtractText("task.extra", "category"); + Assert.assertEquals("JSON_VALUE(task.extra FORMAT JSON, '$.category')", result); + } + + @Test + public void testSqlServerJsonExtractText() { + SqlServerDialect dialect = new SqlServerDialect(); + String result = dialect.jsonExtractText("task.extra", "category"); + // SQL Server inherits AbstractDialect default (JSON_VALUE) + Assert.assertEquals("JSON_VALUE(task.extra, '$.category')", result); + } + + @Test + public void testDmJsonExtractText() { + DmDialect dialect = new DmDialect(); + String result = dialect.jsonExtractText("task.extra", "category"); + // DM inherits AbstractDialect default (JSON_VALUE, Oracle compatible) + Assert.assertEquals("JSON_VALUE(task.extra, '$.category')", result); + } + + @Test + public void testKingbaseJsonExtractText() { + KingbaseDialect dialect = new KingbaseDialect(); + String result = dialect.jsonExtractText("task.extra", "category"); + // KingBase is PostgreSQL compatible + Assert.assertEquals("(task.extra->>'category')", result); + } + + @Test + public void testOceanBaseJsonExtractText() { + OceanBaseDialect dialect = new OceanBaseDialect(); + String result = dialect.jsonExtractText("task.extra", "category"); + // OceanBase is MySQL compatible + Assert.assertEquals("JSON_UNQUOTE(JSON_EXTRACT(task.extra, '$.category'))", result); + } + + @Test + public void testJsonExtractTextWithNestedKey() { + MySqlDialect mysql = new MySqlDialect(); + String result = mysql.jsonExtractText("task.extra", "department.name"); + Assert.assertEquals("JSON_UNQUOTE(JSON_EXTRACT(task.extra, '$.department.name'))", result); + } +} diff --git a/core/src/test/java/com/alibaba/smart/framework/engine/query/OrderSpecTest.java b/core/src/test/java/com/alibaba/smart/framework/engine/query/OrderSpecTest.java new file mode 100644 index 000000000..5c733c006 --- /dev/null +++ b/core/src/test/java/com/alibaba/smart/framework/engine/query/OrderSpecTest.java @@ -0,0 +1,50 @@ +package com.alibaba.smart.framework.engine.query; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for OrderSpec. + * + * @author SmartEngine Team + */ +public class OrderSpecTest { + + @Test + public void testOrderSpecCreation() { + OrderSpec spec = new OrderSpec("gmtCreate", "gmt_create", OrderSpec.Direction.ASC); + + Assert.assertEquals("gmtCreate", spec.getPropertyName()); + Assert.assertEquals("gmt_create", spec.getColumnName()); + Assert.assertEquals(OrderSpec.Direction.ASC, spec.getDirection()); + } + + @Test + public void testToColumnName() { + OrderSpec spec = new OrderSpec("priority", "priority", OrderSpec.Direction.DESC); + + Assert.assertEquals("priority", spec.toColumnName()); + } + + @Test + public void testDirectionSql() { + OrderSpec ascSpec = new OrderSpec("id", "id", OrderSpec.Direction.ASC); + OrderSpec descSpec = new OrderSpec("id", "id", OrderSpec.Direction.DESC); + + Assert.assertEquals("ASC", ascSpec.getDirectionSql()); + Assert.assertEquals("DESC", descSpec.getDirectionSql()); + } + + @Test + public void testToString() { + OrderSpec spec = new OrderSpec("claimTime", "claim_time", OrderSpec.Direction.DESC); + + Assert.assertEquals("claim_time DESC", spec.toString()); + } + + @Test + public void testDirectionEnum() { + Assert.assertEquals("ASC", OrderSpec.Direction.ASC.getSql()); + Assert.assertEquals("DESC", OrderSpec.Direction.DESC.getSql()); + } +} diff --git a/core/src/test/java/com/alibaba/smart/framework/engine/storage/StorageContextTest.java b/core/src/test/java/com/alibaba/smart/framework/engine/storage/StorageContextTest.java new file mode 100644 index 000000000..4c4b2ba9c --- /dev/null +++ b/core/src/test/java/com/alibaba/smart/framework/engine/storage/StorageContextTest.java @@ -0,0 +1,98 @@ +package com.alibaba.smart.framework.engine.storage; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for StorageContext. + * + * @author SmartEngine Team + */ +public class StorageContextTest { + + @Test + public void testBuilder() { + StorageContext context = StorageContext.builder() + .tenantId("tenant001") + .storageMode(StorageMode.DATABASE) + .build(); + + Assert.assertEquals("tenant001", context.getTenantId()); + Assert.assertEquals(StorageMode.DATABASE, context.getStorageMode()); + } + + @Test + public void testBuilderWithProperties() { + StorageContext context = StorageContext.builder() + .tenantId("tenant002") + .storageMode(StorageMode.CUSTOM) + .property("key1", "value1") + .property("key2", 123) + .property("key3", true) + .build(); + + Assert.assertEquals("tenant002", context.getTenantId()); + Assert.assertEquals(StorageMode.CUSTOM, context.getStorageMode()); + Assert.assertEquals("value1", context.getProperty("key1")); + Assert.assertEquals(123, context.getProperty("key2")); + Assert.assertEquals(true, context.getProperty("key3")); + } + + @Test + public void testGetPropertyTyped() { + StorageContext context = StorageContext.builder() + .property("stringKey", "stringValue") + .property("intKey", 42) + .property("boolKey", false) + .build(); + + String strVal = context.getProperty("stringKey", String.class); + Integer intVal = context.getProperty("intKey", Integer.class); + Boolean boolVal = context.getProperty("boolKey", Boolean.class); + + Assert.assertEquals("stringValue", strVal); + Assert.assertEquals(Integer.valueOf(42), intVal); + Assert.assertEquals(Boolean.FALSE, boolVal); + } + + @Test + public void testGetPropertyNull() { + StorageContext context = StorageContext.builder().build(); + + Assert.assertNull(context.getProperty("nonexistent")); + Assert.assertNull(context.getProperty("nonexistent", String.class)); + } + + @Test + public void testGetProperties() { + StorageContext context = StorageContext.builder() + .property("a", 1) + .property("b", 2) + .build(); + + Assert.assertEquals(2, context.getProperties().size()); + Assert.assertTrue(context.getProperties().containsKey("a")); + Assert.assertTrue(context.getProperties().containsKey("b")); + } + + @Test + public void testEmptyContext() { + StorageContext context = StorageContext.builder().build(); + + Assert.assertNull(context.getTenantId()); + Assert.assertNull(context.getStorageMode()); + Assert.assertTrue(context.getProperties().isEmpty()); + } + + @Test + public void testToString() { + StorageContext context = StorageContext.builder() + .tenantId("tenant001") + .storageMode(StorageMode.DUAL_WRITE) + .build(); + + String str = context.toString(); + Assert.assertTrue(str.contains("tenant001")); + Assert.assertTrue(str.contains("DUAL_WRITE")); + } +} diff --git a/core/src/test/java/com/alibaba/smart/framework/engine/storage/StorageModeHolderTest.java b/core/src/test/java/com/alibaba/smart/framework/engine/storage/StorageModeHolderTest.java new file mode 100644 index 000000000..c5ecd5825 --- /dev/null +++ b/core/src/test/java/com/alibaba/smart/framework/engine/storage/StorageModeHolderTest.java @@ -0,0 +1,123 @@ +package com.alibaba.smart.framework.engine.storage; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * Unit tests for StorageModeHolder. + * + * @author SmartEngine Team + */ +public class StorageModeHolderTest { + + @Before + public void setUp() { + StorageModeHolder.clearAll(); + } + + @After + public void tearDown() { + StorageModeHolder.clearAll(); + } + + @Test + public void testSetAndGet() { + Assert.assertNull(StorageModeHolder.get()); + + StorageModeHolder.set(StorageMode.DATABASE); + Assert.assertEquals(StorageMode.DATABASE, StorageModeHolder.get()); + + StorageModeHolder.set(StorageMode.CUSTOM); + Assert.assertEquals(StorageMode.CUSTOM, StorageModeHolder.get()); + } + + @Test + public void testClear() { + StorageModeHolder.set(StorageMode.DUAL_WRITE); + Assert.assertNotNull(StorageModeHolder.get()); + + StorageModeHolder.clear(); + Assert.assertNull(StorageModeHolder.get()); + } + + @Test + public void testSetNull() { + StorageModeHolder.set(StorageMode.DATABASE); + Assert.assertNotNull(StorageModeHolder.get()); + + StorageModeHolder.set(null); + Assert.assertNull(StorageModeHolder.get()); + } + + @Test + public void testGetOrDefault() { + Assert.assertEquals(StorageMode.DATABASE, + StorageModeHolder.getOrDefault(StorageMode.DATABASE)); + + StorageModeHolder.set(StorageMode.CUSTOM); + Assert.assertEquals(StorageMode.CUSTOM, + StorageModeHolder.getOrDefault(StorageMode.DATABASE)); + } + + @Test + public void testSetContext() { + StorageContext context = StorageContext.builder() + .tenantId("tenant001") + .storageMode(StorageMode.DUAL_WRITE) + .property("key1", "value1") + .build(); + + StorageModeHolder.setContext(context); + + Assert.assertNotNull(StorageModeHolder.getContext()); + Assert.assertEquals("tenant001", StorageModeHolder.getContext().getTenantId()); + Assert.assertEquals(StorageMode.DUAL_WRITE, StorageModeHolder.getContext().getStorageMode()); + + // Mode should also be set from context + Assert.assertEquals(StorageMode.DUAL_WRITE, StorageModeHolder.get()); + } + + @Test + public void testSetContextNull() { + StorageContext context = StorageContext.builder() + .storageMode(StorageMode.CUSTOM) + .build(); + StorageModeHolder.setContext(context); + Assert.assertNotNull(StorageModeHolder.getContext()); + + StorageModeHolder.setContext(null); + Assert.assertNull(StorageModeHolder.getContext()); + } + + @Test + public void testClearAll() { + StorageModeHolder.set(StorageMode.DATABASE); + StorageModeHolder.setContext(StorageContext.builder().tenantId("test").build()); + + StorageModeHolder.clearAll(); + + Assert.assertNull(StorageModeHolder.get()); + Assert.assertNull(StorageModeHolder.getContext()); + } + + @Test + public void testThreadIsolation() throws InterruptedException { + StorageModeHolder.set(StorageMode.DATABASE); + + Thread otherThread = new Thread(() -> { + // Should be null in other thread + Assert.assertNull(StorageModeHolder.get()); + + StorageModeHolder.set(StorageMode.CUSTOM); + Assert.assertEquals(StorageMode.CUSTOM, StorageModeHolder.get()); + }); + + otherThread.start(); + otherThread.join(); + + // Original thread should still have DATABASE + Assert.assertEquals(StorageMode.DATABASE, StorageModeHolder.get()); + } +} diff --git a/core/src/test/java/com/alibaba/smart/framework/engine/storage/StorageRegistryTest.java b/core/src/test/java/com/alibaba/smart/framework/engine/storage/StorageRegistryTest.java new file mode 100644 index 000000000..c58d86f7d --- /dev/null +++ b/core/src/test/java/com/alibaba/smart/framework/engine/storage/StorageRegistryTest.java @@ -0,0 +1,141 @@ +package com.alibaba.smart.framework.engine.storage; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * Unit tests for StorageRegistry. + * + * @author SmartEngine Team + */ +public class StorageRegistryTest { + + private StorageRegistry registry; + + // Mock storage interface for testing + interface MockStorage { + String getData(); + } + + // Mock implementation + static class MockDatabaseStorage implements MockStorage { + @Override + public String getData() { + return "database"; + } + } + + static class MockMemoryStorage implements MockStorage { + @Override + public String getData() { + return "memory"; + } + } + + @Before + public void setUp() { + registry = new StorageRegistry(); + } + + @Test + public void testRegisterAndGet() { + MockStorage dbStorage = new MockDatabaseStorage(); + MockStorage memStorage = new MockMemoryStorage(); + + registry.register(StorageMode.DATABASE, MockStorage.class, dbStorage); + registry.register(StorageMode.CUSTOM, MockStorage.class, memStorage); + + MockStorage retrieved1 = registry.get(StorageMode.DATABASE, MockStorage.class); + MockStorage retrieved2 = registry.get(StorageMode.CUSTOM, MockStorage.class); + + Assert.assertSame(dbStorage, retrieved1); + Assert.assertSame(memStorage, retrieved2); + } + + @Test + public void testGetNotFound() { + MockStorage result = registry.get(StorageMode.DATABASE, MockStorage.class); + Assert.assertNull(result); + } + + @Test + public void testContains() { + Assert.assertFalse(registry.contains(StorageMode.DATABASE, MockStorage.class)); + + registry.register(StorageMode.DATABASE, MockStorage.class, new MockDatabaseStorage()); + + Assert.assertTrue(registry.contains(StorageMode.DATABASE, MockStorage.class)); + Assert.assertFalse(registry.contains(StorageMode.CUSTOM, MockStorage.class)); + } + + @Test + public void testRemove() { + MockStorage storage = new MockDatabaseStorage(); + registry.register(StorageMode.DATABASE, MockStorage.class, storage); + + Assert.assertTrue(registry.contains(StorageMode.DATABASE, MockStorage.class)); + + MockStorage removed = registry.remove(StorageMode.DATABASE, MockStorage.class); + + Assert.assertSame(storage, removed); + Assert.assertFalse(registry.contains(StorageMode.DATABASE, MockStorage.class)); + } + + @Test + public void testRemoveNotFound() { + MockStorage removed = registry.remove(StorageMode.DATABASE, MockStorage.class); + Assert.assertNull(removed); + } + + @Test + public void testClear() { + registry.register(StorageMode.DATABASE, MockStorage.class, new MockDatabaseStorage()); + registry.register(StorageMode.CUSTOM, MockStorage.class, new MockMemoryStorage()); + + Assert.assertTrue(registry.contains(StorageMode.DATABASE, MockStorage.class)); + Assert.assertTrue(registry.contains(StorageMode.CUSTOM, MockStorage.class)); + + registry.clear(); + + Assert.assertFalse(registry.contains(StorageMode.DATABASE, MockStorage.class)); + Assert.assertFalse(registry.contains(StorageMode.CUSTOM, MockStorage.class)); + } + + @Test + public void testClearByMode() { + registry.register(StorageMode.DATABASE, MockStorage.class, new MockDatabaseStorage()); + registry.register(StorageMode.CUSTOM, MockStorage.class, new MockMemoryStorage()); + + registry.clear(StorageMode.DATABASE); + + Assert.assertFalse(registry.contains(StorageMode.DATABASE, MockStorage.class)); + Assert.assertTrue(registry.contains(StorageMode.CUSTOM, MockStorage.class)); + } + + // Another mock storage interface for testing multiple types + interface AnotherStorage { + int getCount(); + } + + static class AnotherStorageImpl implements AnotherStorage { + @Override + public int getCount() { + return 42; + } + } + + @Test + public void testMultipleStorageTypes() { + registry.register(StorageMode.DATABASE, MockStorage.class, new MockDatabaseStorage()); + registry.register(StorageMode.DATABASE, AnotherStorage.class, new AnotherStorageImpl()); + + MockStorage mock = registry.get(StorageMode.DATABASE, MockStorage.class); + AnotherStorage another = registry.get(StorageMode.DATABASE, AnotherStorage.class); + + Assert.assertNotNull(mock); + Assert.assertNotNull(another); + Assert.assertEquals("database", mock.getData()); + Assert.assertEquals(42, another.getCount()); + } +} diff --git a/core/src/test/java/com/alibaba/smart/framework/engine/storage/StorageStrategyTest.java b/core/src/test/java/com/alibaba/smart/framework/engine/storage/StorageStrategyTest.java new file mode 100644 index 000000000..afed3cdf4 --- /dev/null +++ b/core/src/test/java/com/alibaba/smart/framework/engine/storage/StorageStrategyTest.java @@ -0,0 +1,161 @@ +package com.alibaba.smart.framework.engine.storage; + +import com.alibaba.smart.framework.engine.storage.strategy.DatabaseStorageStrategy; +import com.alibaba.smart.framework.engine.storage.strategy.CustomStorageStrategy; +import com.alibaba.smart.framework.engine.storage.strategy.DualStorageStrategy; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for StorageStrategy implementations. + * + * @author SmartEngine Team + */ +public class StorageStrategyTest { + + // Mock storage interface for testing + interface MockStorage { + String getValue(); + } + + // ============ DatabaseStorageStrategy Tests ============ + + @Test + public void testDatabaseStorageStrategyMode() { + DatabaseStorageStrategy strategy = new DatabaseStorageStrategy(); + Assert.assertEquals(StorageMode.DATABASE, strategy.getStorageMode()); + } + + @Test + public void testDatabaseStorageStrategySupportsAllTypes() { + DatabaseStorageStrategy strategy = new DatabaseStorageStrategy(); + + Assert.assertTrue(strategy.supportsStorageType(MockStorage.class)); + Assert.assertTrue(strategy.supportsStorageType(String.class)); + Assert.assertTrue(strategy.supportsStorageType(Object.class)); + } + + @Test + public void testDatabaseStorageStrategyPriority() { + DatabaseStorageStrategy strategy = new DatabaseStorageStrategy(); + Assert.assertEquals(0, strategy.getPriority()); + } + + // ============ CustomStorageStrategy Tests ============ + + @Test + public void testCustomStorageStrategyMode() { + CustomStorageStrategy strategy = new CustomStorageStrategy(); + Assert.assertEquals(StorageMode.CUSTOM, strategy.getStorageMode()); + } + + @Test + public void testCustomStorageStrategyRegisterAndResolve() { + CustomStorageStrategy strategy = new CustomStorageStrategy(); + + MockStorage mockStorage = () -> "test-value"; + strategy.registerCustomStorage(MockStorage.class, mockStorage); + + Assert.assertTrue(strategy.supportsStorageType(MockStorage.class)); + + MockStorage resolved = strategy.resolveStorage(MockStorage.class, null, null); + Assert.assertSame(mockStorage, resolved); + Assert.assertEquals("test-value", resolved.getValue()); + } + + @Test + public void testCustomStorageStrategyNotSupported() { + CustomStorageStrategy strategy = new CustomStorageStrategy(); + + Assert.assertFalse(strategy.supportsStorageType(MockStorage.class)); + + MockStorage resolved = strategy.resolveStorage(MockStorage.class, null, null); + Assert.assertNull(resolved); + } + + @Test + public void testCustomStorageStrategyRemove() { + CustomStorageStrategy strategy = new CustomStorageStrategy(); + + MockStorage mockStorage = () -> "value"; + strategy.registerCustomStorage(MockStorage.class, mockStorage); + Assert.assertTrue(strategy.supportsStorageType(MockStorage.class)); + + MockStorage removed = strategy.removeCustomStorage(MockStorage.class); + Assert.assertSame(mockStorage, removed); + Assert.assertFalse(strategy.supportsStorageType(MockStorage.class)); + } + + @Test + public void testCustomStorageStrategyClear() { + CustomStorageStrategy strategy = new CustomStorageStrategy(); + + strategy.registerCustomStorage(MockStorage.class, () -> "value1"); + Assert.assertTrue(strategy.supportsStorageType(MockStorage.class)); + + strategy.clear(); + Assert.assertFalse(strategy.supportsStorageType(MockStorage.class)); + } + + @Test + public void testCustomStorageStrategyPriority() { + CustomStorageStrategy strategy = new CustomStorageStrategy(); + Assert.assertEquals(10, strategy.getPriority()); + } + + // ============ DualStorageStrategy Tests ============ + + @Test + public void testDualStorageStrategyMode() { + DatabaseStorageStrategy dbStrategy = new DatabaseStorageStrategy(); + CustomStorageStrategy customStrategy = new CustomStorageStrategy(); + + DualStorageStrategy dualStrategy = new DualStorageStrategy(dbStrategy, customStrategy); + Assert.assertEquals(StorageMode.DUAL_WRITE, dualStrategy.getStorageMode()); + } + + @Test + public void testDualStorageStrategyPriority() { + DatabaseStorageStrategy dbStrategy = new DatabaseStorageStrategy(); + CustomStorageStrategy customStrategy = new CustomStorageStrategy(); + + DualStorageStrategy dualStrategy = new DualStorageStrategy(dbStrategy, customStrategy); + Assert.assertEquals(20, dualStrategy.getPriority()); + } + + @Test + public void testDualStorageStrategySupportsType() { + DatabaseStorageStrategy dbStrategy = new DatabaseStorageStrategy(); + CustomStorageStrategy customStrategy = new CustomStorageStrategy(); + + DualStorageStrategy dualStrategy = new DualStorageStrategy(dbStrategy, customStrategy); + + // Should delegate to database strategy + Assert.assertTrue(dualStrategy.supportsStorageType(MockStorage.class)); + } + + @Test + public void testDualStorageStrategyClearCache() { + DatabaseStorageStrategy dbStrategy = new DatabaseStorageStrategy(); + CustomStorageStrategy customStrategy = new CustomStorageStrategy(); + + DualStorageStrategy dualStrategy = new DualStorageStrategy(dbStrategy, customStrategy); + + // Should not throw + dualStrategy.clearCache(); + } + + // ============ Strategy Priority Ordering ============ + + @Test + public void testStrategyPriorityOrdering() { + DatabaseStorageStrategy dbStrategy = new DatabaseStorageStrategy(); + CustomStorageStrategy customStrategy = new CustomStorageStrategy(); + DualStorageStrategy dualStrategy = new DualStorageStrategy(dbStrategy, customStrategy); + + // Dual should have highest priority + Assert.assertTrue(dualStrategy.getPriority() > customStrategy.getPriority()); + Assert.assertTrue(customStrategy.getPriority() > dbStrategy.getPriority()); + } +} diff --git a/docs/00-intro/README.md b/docs/00-intro/README.md new file mode 100644 index 000000000..36d1428fc --- /dev/null +++ b/docs/00-intro/README.md @@ -0,0 +1,55 @@ +# SmartEngine 开源项目文档(全新整理) + +> 基于 SmartEngine dev 分支源码(含单元测试)整理。版本参考:`3.7.0-SNAPSHOT`(见根 `pom.xml`)。 + +## 这份文档写给谁 + +- **业务系统开发者**:想把 SmartEngine 嵌入现有系统,快速跑通编排/流程/待办。 +- **平台/中台团队**:需要理解执行模型、并行网关并发语义、存储与扩展点,做二次开发。 +- **贡献者**:需要模块结构、构建测试方式、扩展点规范与发布流程。 + +## 你该从哪里读起 + +- 只想**10 分钟跑通** + - Custom 模式:`01-getting-started/quickstart-custom.md` + - DataBase 模式:`01-getting-started/quickstart-database.md` +- 想先理解**概念与边界**:`02-concepts/` +- 想看**API 用法/变量/跳转/并发**:`03-usage/` +- 想落地**MySQL/PostgreSQL + MyBatis**:`04-persistence/` +- 想做**扩展/二开**:`05-extensibility/` +- 想做**运维与治理**:`06-ops/` +- 想参与**贡献/发布**:`07-dev/` + +## 文档结构 + +本仓库文档按目录组织: + +- `00-intro/`:入口、术语、FAQ +- `01-getting-started/`:快速上手(Custom / DataBase)与示例索引 +- `02-concepts/`:模式、BPMN 子集、执行模型、领域模型 +- `03-usage/`:建模指南、API 指南、变量与高级特性 +- `04-persistence/`:存储分层、表结构、MyBatis 注意事项 +- `05-extensibility/`:扩展点、解析器、行为、存储、用户集成与并发 +- `06-ops/`:性能、失败处理、监控、数据保留 +- `07-dev/`:架构、构建测试、贡献、发布 + +## 重要提示(先读这条) + +1. **SmartEngine 是“轻量业务编排引擎”**:核心接口以 CQRS 风格拆分为 *Command/Query* 服务(见 `com.alibaba.smart.framework.engine.SmartEngine`)。 +2. **两种运行模式**: + - **Custom 模式**:偏“嵌入式 + 自定义存储”,适合你已经有自己的持久化/任务体系。 + - **DataBase 模式**:引擎提供一套关系型存储(MyBatis),并可支持 userTask 待办等增强权限。 +3. **集群一致性**:RepositoryCommandService 将流程定义加载到 **本地内存**(单机)。在集群环境必须自行设计“部署/缓存一致性策略”(见 `02-concepts/modes.md` & `00-intro/faq.md`)。 + +## 快速索引 + +- 术语:`00-intro/glossary.md` +- FAQ:`00-intro/faq.md` +- 支持的 BPMN 子集:`02-concepts/bpmn-subset.md` +- 执行模型(token/执行实例/并发语义):`02-concepts/execution-model.md` +- API 全览(start/signal/query/abort/jump/retry):`03-usage/api-guide.md` +- 数据库表结构:`04-persistence/database-schema.md` + +--- + +生成日期:2025-12-24 diff --git a/docs/00-intro/faq.md b/docs/00-intro/faq.md new file mode 100644 index 000000000..ce99590e0 --- /dev/null +++ b/docs/00-intro/faq.md @@ -0,0 +1,74 @@ +# FAQ + +## 1. Custom vs DataBase 模式怎么选? + +优先按“你想让引擎负责多少事情”来选: + +- 选 **Custom**:你已经有自己的任务/用户/变量/存储体系,或者希望完全控制持久化与事务;SmartEngine 只负责“解析 + 执行 + 并发语义 + 行为扩展”。 +- 选 **DataBase**:你需要一套开箱即用的关系库存储,尤其是 userTask 待办、任务分派、变量持久化、督办/通知等增强权限。 + +详见:`02-concepts/modes.md` + +## 2. 集群环境下怎么保证流程定义一致性? + +`RepositoryCommandService` 的 deploy 会把 BPMN 解析结果加载到**本地内存**(单机)。集群下你需要自行实现: + +- **统一部署源**:例如将 BPMN 存储到数据库(DataBase 模式的 `DeploymentInstance`)、配置中心、对象存储等。 +- **一致性策略**:常见做法: + - 每个节点启动时加载一遍全部 active deployment(冷启动一致)。 + - 通过消息/事件(如 Kafka、Redis pubsub)通知各节点 reload(热更新一致)。 + - 给流程定义增加版本号与缓存 key,确保滚动发布时“新旧版本可并存”。 + +## 3. 并行网关 join 的语义是什么?会不会线程不安全? + +SmartEngine 并行网关在运行期会出现多个 ExecutionInstance;join 的关键在于: + +- 如何判断“所有分支都到达 join”; +- 以及如何避免多线程同时通过 join 造成重复推进。 + +源码中与并行相关的关键点: + +- `com.alibaba.smart.framework.engine.util.ParallelGatewayUtil`:线程池选择、latch 等控制参数解析 +- `ParallelGatewayBehavior` & 相关常量:并行网关行为与属性 +- `LockStrategy`:旧的锁扩展点(Deprecated) + +建议阅读:`02-concepts/execution-model.md`、`05-extensibility/concurrency.md`。 + +## 4. Postgres / MySQL 兼容如何处理?类型坑多吗? + +仓库提供 **MySQL 与 PostgreSQL 两套 DDL**: + +- `extension/storage/storage-mysql/src/main/resources/sql/schema.sql` +- `extension/storage/storage-mysql/src/main/resources/sql/schema-postgre.sql` +- 以及工作流增强表:`workflow-enhancement-schema-*.sql` + +但在运行时仍需注意: + +- MyBatis 参数类型与列类型不一致会触发 Postgres 的 `operator does not exist`(例如 bigint vs varchar); +- boolean / smallint 映射差异; +- in (...) 参数展开与 jdbcType/类型处理器。 + +详见:`04-persistence/mybatis-notes.md`。 + +## 5. 变量(Variable)持久化是怎么做的?作用域是什么? + +变量相关接口集中在: + +- Command:`VariableCommandService` +- Query:`VariableQueryService` +- 持久化策略:`VariablePersister`(默认 `DefaultVariablePersister`) + +并且 Request/Context 中存在一组“特殊 key”(如 tenantId、task 扩展字段等),见 `RequestMapSpecialKeyConstant`。详见:`03-usage/variables.md`。 + +## 6. 引擎异常发生后会怎样?有没有重试权限? + +- 引擎层面的异常处理由 `ExceptionProcessor` 统一入口处理(默认 `DefaultExceptionProcessor`)。 +- 仓库提供 retry 扩展模块(`extension/retry/*`),并有 `@Retryable` 注解用于声明最大次数与 delay。 + +详见:`06-ops/failure-handling.md` 与 `05-extensibility/overview.md`。 + +## 7. SmartEngine 是否必须依赖 Spring Boot? + +不必须。核心是纯 Java 组件;测试里既有纯 JUnit,也有 Spring XML 配置的示例(例如 `storage-mysql/src/test/resources/spring/application-test.xml`)。 +你可以在 Spring Boot 中更方便地装配 DataSource/MyBatis/Bean 扫描,但不是必选项。 + diff --git a/docs/00-intro/glossary.md b/docs/00-intro/glossary.md new file mode 100644 index 000000000..bfcb3ec1a --- /dev/null +++ b/docs/00-intro/glossary.md @@ -0,0 +1,47 @@ +# 术语表(Glossary) + +本术语表以 SmartEngine 源码命名为准(核心 model 在 `core/src/main/java/com/alibaba/smart/framework/engine/model`)。 + +## 流程与定义类 + +- **Process Definition(流程定义)**:BPMN XML 解析后在引擎内存中的模型。对应 `model/assembly/ProcessDefinition`。 +- **ProcessDefinitionId / Version**:流程定义的标识与版本(在 API 中经常同时出现)。 +- **ProcessDefinitionSource**:部署/解析后返回的容器对象,包含 definition 及元信息。对应 `model/assembly/ProcessDefinitionSource`。 +- **DeploymentInstance(部署实例)**:DataBase 模式下,流程定义文件的持久化记录(数据库中保存 BPMN 内容等),并负责触发 Repository 解析加载。对应 `model/instance/DeploymentInstance`,表 `se_deployment_instance`。 + +## 运行期实例类 + +- **ProcessInstance(流程实例)**:一次流程运行的顶层实例。对应 `model/instance/ProcessInstance`,表 `se_process_instance`。 +- **ExecutionInstance(执行实例)**:更细粒度的“token/执行上下文”载体;并行网关分叉后会出现多个活跃 execution。对应 `model/instance/ExecutionInstance`,表 `se_execution_instance`。 +- **ActivityInstance(活动实例)**:某个 BPMN 节点(activity)在运行期的实例化记录。对应 `model/instance/ActivityInstance`,表 `se_activity_instance`。 +- **TaskInstance(任务实例)**:主要对应 userTask 的待办/任务记录(DataBase 模式增强权限)。对应 `model/instance/TaskInstance`,表 `se_task_instance`。 +- **TaskAssigneeInstance(任务分派实例)**:任务的处理人/候选人/候选组等分派记录。对应 `model/instance/TaskAssigneeInstance` 等,表 `se_task_assignee_instance`。 +- **VariableInstance(变量实例)**:持久化的流程变量记录。对应 `model/instance/VariableInstance`,表 `se_variable_instance`。 +- **TransitionInstance(流转实例)**:描述从一个节点到另一个节点的流转轨迹(更偏运行期内部)。对应 `model/instance/TransitionInstance`。 + +## 行为与执行 + +- **ActivityBehavior(节点行为)**:每类 BPMN activity 的执行逻辑(ServiceTask/ReceiveTask/UserTask/网关等),位于 `engine/behavior/*`。 +- **Delegation(委派执行)**:SmartEngine 对“调用业务代码”的抽象,常见于 `smart:class` / ServiceTask。由 `DelegationExecutor` 执行。 +- **Listener(监听器)**:节点/执行监听扩展点(ExecutionListener 等),由 `ListenerExecutor` 执行。 +- **ExecutionContext(执行上下文)**:信号、变量、租户、扩展字段等运行上下文承载体。见 `engine/context/*`。 + +## 模式与存储 + +- **Custom 模式**:通过 `InstanceAccessor` 等机制把引擎与业务系统的 bean/类加载集成,并可使用自定义存储实现(参考 `extension/storage/storage-custom`)。 +- **DataBase 模式**:使用 `extension/storage/storage-mysql` 提供的关系库存储实现(含 `schema.sql` / `schema-postgre.sql` 与 MyBatis SQLMap)。 +- **InstanceAccessor**:引擎获取“业务对象/bean/类实例”的统一入口。Custom 与 DataBase 测试里都给出示例实现。 +- **Storage(存储接口)**:`core/.../instance/storage/*` 定义流程实例、执行实例、变量、任务等持久化接口;不同模块提供实现。 + +## 并发与网关 + +- **Parallel Gateway(并行网关)**:分叉会创建多个 execution;join 需要等待全部到达点,涉及并发控制与线程池选择(见 `util/ParallelGatewayUtil`)。 +- **LockStrategy(锁策略)**:较早的锁扩展点,源码标记为 Deprecated(粒度太小,不建议继续扩展)。 +- **ExecutorService / poolsMap**:并行网关可选择默认线程池或按节点 properties 指定线程池。 + +## 工作流增强(Enhancement) + +- **NotificationInstance(通知实例)**:通知类记录(DataBase 模式),表 `se_notification_instance`。 +- **SupervisionInstance(督办实例)**:督办类记录(DataBase 模式),表 `se_supervision_instance`。 +- **Transfer / Rollback Record**:任务转派、回退等增强表,位于 `workflow-enhancement-schema-*.sql`。 + diff --git a/docs/01-getting-started/examples.md b/docs/01-getting-started/examples.md new file mode 100644 index 000000000..024756d01 --- /dev/null +++ b/docs/01-getting-started/examples.md @@ -0,0 +1,23 @@ +# 示例清单(Examples) + +SmartEngine 的“示例”主要以 **单元测试 / 集成测试** 的形式存在。你可以把它们当成: + +- 可运行的用法样例(示范如何 init / deploy / start / signal / query) +- 对执行语义的可验证说明(并行网关、跳转、回退、兼容性等) + +下表列出最常用的一组示例入口(建议从上到下跑): + +| 场景 | BPMN 文件 | 关联测试(建议直接打开阅读) | +|---|---|---| +| 并行网关 + ReceiveTask 并发推进 | `test-receivetask-parallel-gateway.bpmn20.xml` | storage-custom: BasicParallelGatewayTest, ConcurrentParallelGatewayTest
storage-mysql: ConcurrentParallelGatewayDBTest, ConcurrentParallelGatewayDBWithTenantIdTest | +| 并行网关 + 全 ServiceTask 编排 | `test-all-servicetask-parallel-gateway.bpmn20.xml` | storage-custom: ServiceOrchestrationParallelGateway* 系列测试 | +| 流程跳转 jumpTo/jumpFrom | `smart-engine/jump1.bpmn20.xml / jump2.bpmn20.xml` | storage-custom: JumpFreeNode1Test, JumpFreeNode2Test | +| CallActivity(父子流程/并行场景) | `parent-callactivity-process.bpmn20.xml + child-callactivity-process.bpmn20.xml` | storage-mysql: callactivity 相关测试资源与用例 | +| userTask 待办 + 分派(用户/组) | `user-task-id-and-group-test.bpmn20.xml` | storage-mysql: TaskServiceTest, TaskServiceWithTenantTest | +| 变量服务(增删改查/持久化) | `user-task-id-and-group-test.bpmn20.xml` | storage-mysql: VariableServiceTest, VariableServiceWithTenantTest | +| 命名空间兼容(Camunda/Flowable/Activiti) | `core/src/test/resources/process-def/extension/*.bpmn20.xml` | core: ExtensionNameSpaceParseTest 等 | +| 扩展属性/扩展元素解析 | `core/src/test/resources/process-def/extend/extend.bpmn20.xml` | core: ExtenstionTest 等 | +| 消息队列/ReceiveTask 场景 | `message-queue-receive-task.bpmn20.xml` | storage-mysql: process 相关测试 | +| 会签/多实例(multi-instance) | `multi-instance-test.bpmn20.xml / multi-instance-activiti-compatiable-test.bpmn20.xml` | storage-mysql: multi-instance 相关测试 | + +> 备注:测试类具体路径请在源码中按类名搜索。DataBase 模式多数测试需要本地数据库(参考 `storage-mysql/src/test/resources/spring/application-test.xml`)。 diff --git a/docs/01-getting-started/quickstart-custom.md b/docs/01-getting-started/quickstart-custom.md new file mode 100644 index 000000000..cf310fc6a --- /dev/null +++ b/docs/01-getting-started/quickstart-custom.md @@ -0,0 +1,124 @@ +# Quickstart(Custom 模式)— 10 分钟跑通 + +本章目标:在不依赖数据库(或不依赖引擎自带表结构)的前提下,完成一次 **deploy → start → signal → query** 的最小闭环。 + +> 对应源码参考:`extension/storage/storage-custom/src/test/java/.../CustomBaseTestCase` 及其子测试用例。 + +## 1. 引入依赖 + +Maven 示例(按需选择版本): + +```xml + + com.alibaba.smart.framework + smart-engine-core + 3.7.0-SNAPSHOT + + + + + com.alibaba.smart.framework + smart-engine-extension-storage-custom + 3.7.0-SNAPSHOT + +``` + +## 2. 准备一份最小 BPMN + +下面是一个最小流程:Start → ServiceTask → End。 + +```xml + + + + + + + + + + + + + + +``` + +说明: + +- `smart:class` 会触发引擎在执行节点时,通过 `DelegationExecutor` 调用你的业务类(详见 `05-extensibility/behaviors.md`)。 +- SmartEngine 还做了命名空间兼容(Camunda/Flowable/Activiti),见 `DefaultProcessEngineConfiguration` 的 magicExtension fallback(`02-concepts/bpmn-subset.md`)。 + +## 3. 初始化引擎(核心步骤) + +Custom 模式你通常至少要提供: + +- `ProcessEngineConfiguration`:引擎配置 +- `InstanceAccessor`:引擎如何拿到业务对象/bean/类实例(用于 delegation、listener 等) + +最小可运行示例(纯 Java): + +```java +import com.alibaba.smart.framework.engine.SmartEngine; +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultSmartEngine; + +public class Demo { + public static void main(String[] args) { + ProcessEngineConfiguration cfg = new DefaultProcessEngineConfiguration(); + + // Custom 模式下非常关键:引擎如何拿到你的业务类/Bean + cfg.setInstanceAccessor(classOrBeanName -> { + try { + return Thread.currentThread().getContextClassLoader() + .loadClass(classOrBeanName).getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + SmartEngine engine = new DefaultSmartEngine(); + engine.init(cfg); + + // 1) deploy(解析到本地内存) + engine.getRepositoryCommandService().deploy("demo.bpmn20.xml"); + + // 2) start + var pi = engine.getProcessCommandService() + .start("demo", "1.0.0", null, null); + + // 3) query active executions + var executions = engine.getExecutionQueryService() + .findActiveExecutionList(pi.getInstanceId()); + + // 4) signal:对 ReceiveTask / 暂停点推进(ServiceTask 一般不需要外部 signal) + // engine.getExecutionCommandService().signal(executions.get(0).getInstanceId(), null); + } +} +``` + +> 说明:`RepositoryCommandService.deploy(...)` 负责解析 XML 并加载到内存。集群环境下需要自行做一致性策略(见 `00-intro/faq.md`)。 + +## 4. 常用 API(你会最先用到的) + +- `RepositoryCommandService`:部署(解析)流程定义 +- `ProcessCommandService`:start / abort / suspend 等 +- `ExecutionCommandService`:signal / jump / markDone / retry 等 +- `*QueryService`:查询实例、任务、变量等 + +完整 API 列表见:`03-usage/api-guide.md` + +## 5. Custom 存储怎么做? + +SmartEngine 把持久化抽象成 Storage 接口(位于 `core/.../instance/storage/*`)。你有两条路: + +1) **完全自己实现**(推荐生产做法):对接你的数据库/缓存/事件日志等。 +2) **参考仓库的 Custom 存储示例**:`extension/storage/storage-custom` 通过 `PersisterSession`(线程内 Map)模拟持久化,主要用于单元测试与理解接口边界。 + +下一步建议阅读: + +- `04-persistence/storage-overview.md` +- `05-extensibility/storage.md` diff --git a/docs/01-getting-started/quickstart-database.md b/docs/01-getting-started/quickstart-database.md new file mode 100644 index 000000000..51eec25b8 --- /dev/null +++ b/docs/01-getting-started/quickstart-database.md @@ -0,0 +1,124 @@ +# Quickstart(DataBase 模式)— 10 分钟跑通(含表结构/待办) + +本章目标:在关系型数据库中完成 **部署 → 启动 → 生成 userTask 待办 → 查询/完成任务** 的最小闭环。 + +> 对应源码参考:`extension/storage/storage-mysql/src/test/java/.../DatabaseBaseTestCase` 与各类 integration tests。 + +## 1. 引入依赖 + +```xml + + com.alibaba.smart.framework + smart-engine-core + 3.7.0-SNAPSHOT + + + + + com.alibaba.smart.framework + smart-engine-extension-storage-mysql + 3.7.0-SNAPSHOT + +``` + +> 注:模块名为 *storage-mysql*,但仓库同时提供 `schema-postgre.sql`,测试 `application-test.xml` 默认也是 PostgreSQL。 + +## 2. 初始化数据库(MySQL / PostgreSQL) + +你需要执行 DDL: + +- 核心表:`sql/schema.sql`(MySQL)或 `sql/schema-postgre.sql`(PostgreSQL) +- 增强表(督办/通知/转派/回退等):`sql/workflow-enhancement-schema-*.sql` +- 索引:`sql/index.sql` +- Retry 扩展(可选):`extension/retry/retry-mysql/src/main/resources/sql/retry-schema.sql` + +本仓库核心表清单: + +- `se_deployment_instance` +- `se_process_instance` +- `se_activity_instance` +- `se_task_instance` +- `se_execution_instance` +- `se_task_assignee_instance` +- `se_variable_instance` +- `se_supervision_instance` +- `se_notification_instance` +- `se_task_transfer_record` +- `se_assignee_operation_record` +- `se_process_rollback_record` + +详见:`04-persistence/database-schema.md` + +## 3. MyBatis / Spring 装配(参考测试配置) + +仓库测试给出一个完整 Spring XML(含 PostgreSQL、MyBatis、事务): +`extension/storage/storage-mysql/src/test/resources/spring/application-test.xml` + +关键点: + +- `mybatis-config.xml`:开启下划线转驼峰、注册 typeAliases(entity + param) +- `mapperLocations`:`classpath:mybatis/sqlmap/*.xml` +- DataSource:可选 MySQL / PostgreSQL / H2(测试用) + +你可以用 Spring JavaConfig / Spring Boot 等价实现,核心是要确保: + +- SQLSessionFactory 能加载到 `mybatis/sqlmap/*.xml` +- 扫描到 `com.alibaba.smart.framework.engine.persister` 下的组件(DAO/Storage/Service) + +## 4. 初始化引擎(必设项) + +DataBase 模式下,建议至少设置: + +- `IdGenerator`:生成实例/任务等 ID(测试使用 `TimeBasedIdGenerator`) +- `InstanceAccessor`:用于 delegation/listener 取业务 bean(测试使用 Spring ApplicationContext) +- `TaskAssigneeDispatcher`:userTask 的分派器(候选人/候选组/组织等),database 模式特别重要 + +参考测试 `DatabaseBaseTestCase.initProcessConfiguration()`: + +```java +ProcessEngineConfiguration cfg = new DefaultProcessEngineConfiguration(); +cfg.setIdGenerator(new TimeBasedIdGenerator()); +cfg.setInstanceAccessor(new DataBaseAccessService()); // 通常对接 Spring 容器 +cfg.setTaskAssigneeDispatcher(new DefaultTaskAssigneeDispatcher()); +``` + +## 5. 一次完整闭环(部署 → 启动 → 查询待办 → 完成) + +示例流程(包含 userTask 的 BPMN)可参考测试资源: + +- `extension/storage/storage-mysql/src/test/resources/user-task-id-and-group-test.bpmn20.xml` + +运行步骤(伪代码): + +```java +SmartEngine engine = new DefaultSmartEngine(); +engine.init(cfg); + +// 1) 创建 deployment(将 BPMN 文件持久化到 DB,并加载到内存) +DeploymentInstance dep = engine.getDeploymentCommandService() + .createDeploymentInstance(new CreateDeploymentCommand() + .setName("demo") + .setProcessDefinitionContent(bpmnXml) + .setStatus("active")); + +// 2) 启动流程实例 +ProcessInstance pi = engine.getProcessCommandService() + .start(dep.getProcessDefinitionId(), dep.getProcessDefinitionVersion(), request, response); + +// 3) 查询待办任务 +List tasks = engine.getTaskQueryService() + .findActiveTaskList(new TaskInstanceQueryParam() + .setProcessInstanceId(pi.getInstanceId())); + +// 4) 认领/完成任务(具体方法见 TaskCommandService) +engine.getTaskCommandService().claim(tasks.get(0).getInstanceId(), "userA"); +engine.getTaskCommandService().complete(tasks.get(0).getInstanceId(), request); +``` + +> 注意:上面 Command/Param 的具体字段以源码为准(`com.alibaba.smart.framework.engine.service.param.*`)。不同接口有 tenantId 重载,建议你在多租户场景下统一通过 request special key 或显式 tenantId 传入。 + +## 6. 下一步建议 + +- 先把 “user/组织/岗位/候选组” 分派逻辑做成你自己的 `TaskAssigneeDispatcher`:`05-extensibility/user-integration.md` +- 阅读 MyBatis 兼容注意事项(尤其是 PostgreSQL):`04-persistence/mybatis-notes.md` +- 理解并行网关并发语义:`05-extensibility/concurrency.md` diff --git a/docs/02-concepts/bpmn-subset.md b/docs/02-concepts/bpmn-subset.md new file mode 100644 index 000000000..f1dc2bce5 --- /dev/null +++ b/docs/02-concepts/bpmn-subset.md @@ -0,0 +1,117 @@ +# 支持的 BPMN 子集与 Smart 扩展 + +本章回答两个问题: + +1) SmartEngine 识别/执行哪些 BPMN 元素? +2) SmartEngine 的扩展(smart:xxx)怎么用?与 Camunda/Flowable/Activiti 扩展是否兼容? + +--- + +## 1. BPMN NameSpace 与兼容策略 + +SmartEngine 默认 BPMN namespace: + +- BPMN:`String NAME_SPACE = "http://www.omg.org/spec/BPMN/20100524/MODEL";`(见 `BpmnNameSpaceConstant.NAME_SPACE`) + +兼容扩展 namespace(常见于 `camunda:class` / `flowable:class` / `activiti:class`): + +- `http://camunda.org/schema/1.0/bpmn` +- `http://flowable.org/bpmn` +- `http://activiti.org/bpmn` + +> 代码位置:`core/.../bpmn/constant/BpmnNameSpaceConstant.java`。 + +### 1.1 “magicExtension fallback” + +`DefaultProcessEngineConfiguration` 中维护了一个“命名空间 fallback 映射”,用于把不同生态的扩展属性映射到 SmartEngine 识别的 key(例如把 camunda/flowable/activiti 的 class 属性映射为统一的 `smart:class` 语义)。 + +这让你可以在迁移现有 BPMN 时,减少改造成本。 + +--- + +## 2. SmartEngine 内置的元素/行为实现 + +SmartEngine 的执行以 `ActivityBehavior` 为核心抽象,内置实现主要来自: + +- `core/.../engine/behavior/impl/*`(任务类) +- `core/.../engine/bpmn/behavior/*`(事件、网关、sequence flow、callActivity) + +### 2.1 事件(Event) + +- StartEvent:`StartEventBehavior` +- EndEvent:`EndEventBehavior` + +### 2.2 Sequence Flow + +- SequenceFlow:`SequenceFlowBehavior` + +### 2.3 网关(Gateway) + +- ExclusiveGateway:`ExclusiveGatewayBehavior` +- ParallelGateway:`ParallelGatewayBehavior` +- InclusiveGateway:`InclusiveGatewayBehavior` + +### 2.4 任务(Task) + +`core/.../engine/behavior/impl` 内置了多种 TaskBehavior: + +- ServiceTask:`ServiceTaskBehavior` +- ReceiveTask:`ReceiveTaskBehavior` +- UserTask:`UserTaskBehavior`(DataBase 模式下结合任务表/分派表更完整) +- 以及 Script/Send/Manual/BusinessRule 等行为类(是否启用取决于你 BPMN 与扩展配置) + +--- + +## 3. Smart 扩展:smart:class / smart:executionListener 等 + +SmartEngine 的扩展 namespace 常量见: + +- `core/.../constant/SmartBase.SMART_NS`(默认:`http://smartengine.org/schema/process`) + +你在 BPMN 中常见的扩展用法包括: + +### 3.1 smart:class(业务委派) + +用于 ServiceTask 等节点执行时,调用你的业务代码。 + +示意: + +```xml + +``` + +引擎执行时大致流程: + +1) 解析阶段把 `smart:class` 记录到节点属性(properties/extension) +2) 执行阶段由 `DelegationExecutor` 通过 `InstanceAccessor` 获取实例并调用 +3) 异常由 `ExceptionProcessor` 处理;可结合 retry 扩展 + +### 3.2 executionListener(执行监听) + +在 BPMN 的 `` 中声明监听器,例如: + +```xml + + + +``` + +监听器执行由 `ListenerExecutor` 统一调度。 + +> 仓库示例:`storage-custom/src/test/resources/DelegationAndListenerExecutorExtensionTest.xml` + +--- + +## 4. 建模建议(先看这一条) + +- 尽量在同一套规范下管理扩展: + - 你可以统一使用 `smart:*` + - 或沿用 Camunda/Flowable/Activiti 的扩展属性,但要明确团队约定 +- 如果你在做“流程建模工具/低代码设计器”,建议把扩展属性抽象成“可配置项”,而不是直接散落在 XML 里(仓库中 `ecology/designer/*` 有相关基础类)。 + +下一步: + +- 执行模型(token / execution / signal):`execution-model.md` +- 扩展点总览:`05-extensibility/overview.md` + diff --git a/docs/02-concepts/domain-model.md b/docs/02-concepts/domain-model.md new file mode 100644 index 000000000..d9f6275e8 --- /dev/null +++ b/docs/02-concepts/domain-model.md @@ -0,0 +1,74 @@ +# 领域对象:Deployment / Definition / Process / Activity / Execution / Task / Variable… + +本章把 SmartEngine 的“代码对象”与“数据库对象”(DataBase 模式)对齐,帮助你: + +- 看懂引擎的 CQRS API 参数与返回值 +- 定位你需要扩展/联动的表与数据 +- 做二次开发(增加字段、做历史归档、做查询优化) + +--- + +## 1. 代码层:模型所在位置 + +- 解析/定义模型:`core/.../model/assembly/*` +- 运行期实例模型:`core/.../model/instance/*` +- Param/Command:`core/.../service/param/*` +- 扩展点与行为:`core/.../engine/*` 与 `core/.../behavior/*` + +## 2. DataBase 模式表与核心对象映射 + +| 表 | 代码对象 | 说明 | +|---|---|---| +| `se_deployment_instance` | `DeploymentInstance` | BPMN 文件持久化记录(可作为集群“定义真相源”) | +| `se_process_instance` | `ProcessInstance` | 流程实例 | +| `se_execution_instance` | `ExecutionInstance` | token/执行分支载体 | +| `se_activity_instance` | `ActivityInstance` | 节点运行期实例 | +| `se_task_instance` | `TaskInstance` | userTask 待办任务(增强) | +| `se_task_assignee_instance` | `TaskAssigneeInstance*` | 候选人/组/组织/岗位等分派记录 | +| `se_variable_instance` | `VariableInstance` | 变量持久化记录 | +| `se_supervision_instance` | `SupervisionInstance` | 督办记录(增强) | +| `se_notification_instance` | `NotificationInstance` | 通知记录(增强) | +| `se_task_transfer_record` | `TaskTransferRecordEntity` | 任务转派记录(增强) | +| `se_assignee_operation_record` | `AssigneeOperationRecordEntity` | 分派操作记录(增强) | +| `se_process_rollback_record` | `ProcessRollbackRecordEntity` | 回退记录(增强) | + +> DDL 详见 `04-persistence/database-schema.md`。 + +## 3. 关联关系(典型查询路径) + +### 3.1 从流程实例到待办 + +```text +ProcessInstance + -> active ExecutionInstance(s) + -> active ActivityInstance(s) + -> TaskInstance(s) (userTask) + -> TaskAssigneeInstance(s) (候选人/候选组/组织岗位) +``` + +### 3.2 从 Deployment 到定义 + +```text +DeploymentInstance + -> process_definition_content (BPMN XML) + -> parse & load -> ProcessDefinitionSource -> ProcessDefinition +``` + +## 4. 多租户(tenant_id) + +表结构里大多包含 `tenant_id` 字段;API 也大量提供 tenantId 重载。 + +同时请求/上下文中还存在一组“特殊 key”(以 `_$_smart_engine_$_` 为前缀)用于传递 tenantId、task 扩展字段等(见 `RequestMapSpecialKeyConstant`)。 + +建议你的系统在入口层统一封装: + +- tenantId 的解析与注入 +- 幂等 key 的注入(如果你希望在 start/signal 上做幂等) + +--- + +下一步: + +- API 使用与幂等建议:`03-usage/api-guide.md` +- 数据库表结构与索引:`04-persistence/database-schema.md` + diff --git a/docs/02-concepts/execution-model.md b/docs/02-concepts/execution-model.md new file mode 100644 index 000000000..07964b326 --- /dev/null +++ b/docs/02-concepts/execution-model.md @@ -0,0 +1,106 @@ +# 执行模型:token / ExecutionInstance / 暂停点 / signal 语义 + +SmartEngine 的运行期核心在于:**一个流程实例 ProcessInstance,内部可能有多个 ExecutionInstance(token)并行推进**。 + +本章从“你写业务系统会遇到的问题”出发,解释: + +- token 是什么、什么时候会变多 +- ReceiveTask/暂停点如何通过 signal 推进 +- 并行网关 join 为什么需要并发控制 +- ActivityInstance / TransitionInstance 在哪里发挥作用 + +--- + +## 1. 核心对象关系(概念图) + +```mermaid +flowchart LR + PI[ProcessInstance] --> EI1[ExecutionInstance #1] + PI --> EI2[ExecutionInstance #2] + EI1 --> AI1[ActivityInstance] + EI2 --> AI2[ActivityInstance] + AI1 --> TI1[TransitionInstance] + AI2 --> TI2[TransitionInstance] + PI --> VAR[VariableInstance*] + PI --> TASK[TaskInstance*] + TASK --> ASS[TaskAssigneeInstance*] +``` + +- 并行分叉会使 ExecutionInstance 数量增加 +- join 需要等待多个 ExecutionInstance 到达同一网关 + +--- + +## 2. token(ExecutionInstance)到底是什么? + +在 SmartEngine 中,ExecutionInstance 可以理解为: + +- “当前执行推进的位置 + 上下文” +- “并行时的分支载体” + +典型场景: + +- **顺序流**:一般只有一个活跃 execution +- **并行网关分叉**:会创建多个 execution(每个分支一个) +- **并行网关 join**:多个 execution 汇合为一个推进点 + +--- + +## 3. ActivityInstance 与暂停点(ReceiveTask) + +### 3.1 ReceiveTask 为什么需要 signal? + +ReceiveTask 的语义通常是: + +- 引擎执行到该节点后进入等待状态 +- 外部事件到来后,通过 `ExecutionCommandService.signal(...)` 继续推进 + +也就是说:**ReceiveTask 不是自动推进的,它需要外部触发**。 + +### 3.2 signal 的对象是谁? + +signal 通常作用在: + +- 某个 execution(executionInstanceId) +- 或某个 processInstance(由引擎内部定位 active execution) + +具体重载方法详见 `ExecutionCommandService`(见 `03-usage/api-guide.md`)。 + +--- + +## 4. 并行网关 join:为什么“最容易踩坑”? + +并行 join 的两类常见坑: + +1) **重复推进**:多个线程/节点同时判断“已满足 join 条件”,导致后续节点被执行多次 +2) **丢推进**:某些分支状态未正确落库/未正确计数,导致 join 永远等不到 + +SmartEngine 在并行处理上引入了: + +- 可配置的 ExecutorService(按节点属性或默认线程池) +- latch 等等待控制(见 `ParallelGatewayUtil`) +- 以及历史遗留的锁扩展点 `LockStrategy`(Deprecated) + +建议阅读:`05-extensibility/concurrency.md` + +--- + +## 5. “完成/跳过”语义:markDone / jump + +除了标准推进外,SmartEngine 还提供: + +- `markDone`:把某个 execution / activity 标记完成(常见于补偿或人工处理) +- `jumpFrom` / `jumpTo`:流程跳转(见 `03-usage/jump-and-advanced.md`) + +这些权限在“异常补偿、人工干预、重跑”场景非常关键,但也意味着你必须: + +- 明确幂等(避免重复跳转) +- 明确数据一致性(尤其是 DataBase 模式下任务/变量) + +--- + +下一步: + +- 领域对象与表结构:`domain-model.md` +- 并发专题:`05-extensibility/concurrency.md` + diff --git a/docs/02-concepts/modes.md b/docs/02-concepts/modes.md new file mode 100644 index 000000000..1de374a40 --- /dev/null +++ b/docs/02-concepts/modes.md @@ -0,0 +1,90 @@ +# 运行模式:Custom vs DataBase + +SmartEngine 从源码层面把“引擎核心语义”与“存储/集成实现”拆开: + +- 核心执行权限在 `core/`(解析、执行模型、行为、服务接口、扩展机制) +- 存储与增强权限通过 `extension/` 提供(Custom / DataBase / Retry 等) + +本章说明两种最常见的落地模式,以及它们的边界。 + +--- + +## 1. 一句话对比 + +| 维度 | Custom 模式 | DataBase 模式 | +|---|---|---| +| 流程定义存储 | 通常由你自己决定;`RepositoryCommandService` 只负责“解析并加载到本地内存” | 流程定义可通过 `DeploymentInstance` 持久化到 DB(表 `se_deployment_instance`),并触发加载 | +| 运行期实例 | 你可自己实现 Storage 接口(或只做内存) | 引擎提供关系库实现:process/execution/activity/task/variable… | +| userTask 待办 | 由你系统已有待办体系承担 | 引擎提供任务表 + 分派表(`se_task_instance`/`se_task_assignee_instance`)与分派扩展点 | +| 事务边界 | 由你自己决定(可与业务事务合并) | 通常以 DB 事务为边界(MyBatis + Spring Tx) | +| 集群一致性 | 需要你自行做“定义一致性/缓存一致性” | 仍需要一致性策略,但有 DB deployment 作为统一真相源更易实现 | +| 适用场景 | 业务系统“已有一切”,只需要引擎语义(轻量/可控) | 想要开箱即用的流程 + 存储 + 待办增强 | + +--- + +## 2. Custom 模式的“核心约束” + +### 2.1 Repository 只负责“加载到本地内存” + +`RepositoryCommandService` 的 deploy 是“把 BPMN XML 解析为内存模型”: + +- 单机 OK +- 集群必须解决“每台机器都加载同一套 definition”的一致性 + +典型做法: + +1) BPMN 文件存到 DB/配置中心/对象存储 +2) 通过发布事件通知所有节点 reload +3) 或服务启动时扫描并预热缓存 + +### 2.2 你需要实现(或决定)这些集成点 + +最重要的三个: + +- `InstanceAccessor`:引擎如何拿到业务 bean/类实例(delegation、listener 用) +- Storage 接口实现(可选但生产强烈建议):`core/.../instance/storage/*` +- 变量持久化策略(可选):`VariablePersister` + +仓库的 Custom 模式实现(用于测试)在: + +- `extension/storage/storage-custom`(基于 `PersisterSession` 的线程内 Map) + +> 注意:它更像“教学/测试存根”,不适合直接上生产。 + +--- + +## 3. DataBase 模式的“核心约束” + +### 3.1 DataBase 模式到底提供了什么? + +它提供“关系库存储实现 + 一组工作流增强表 + MyBatis SQLMap”: + +- 存储实现类:`extension/storage/storage-mysql/.../RelationshipDatabase*Storage` +- SQLMap:`extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/*.xml` +- DDL:`extension/storage/storage-mysql/src/main/resources/sql/*` + +### 3.2 仍然要你提供的东西 + +- `IdGenerator`:生成 instanceId/taskId(测试默认 `TimeBasedIdGenerator`) +- `InstanceAccessor`:通常对接 Spring 容器 +- `TaskAssigneeDispatcher`:决定 userTask 的候选人/候选组/组织岗位等规则 +- 集群下的部署一致性(推荐:以 `DeploymentInstance` 表为真相源,做全量加载或增量通知) + +--- + +## 4. 模式选择建议(经验法则) + +- 如果你有成熟的:**用户/组织、待办、变量、审计、存储、权限** → 选 **Custom** +- 如果你想先跑通 MVP,后续再逐步替换存储/待办 → 先用 **DataBase**(更快) +- 如果你要做“平台化、可插拔” → 推荐: + - 核心语义沿用 SmartEngine + - 存储接口自研实现(兼容多 DB、历史表、归档等) + - 任务分派与权限深度融合(RBAC/ABAC) + +--- + +下一步: + +- 支持的 BPMN 子集:`bpmn-subset.md` +- 执行模型:`execution-model.md` + diff --git a/docs/03-usage/api-guide.md b/docs/03-usage/api-guide.md new file mode 100644 index 000000000..99add2282 --- /dev/null +++ b/docs/03-usage/api-guide.md @@ -0,0 +1,365 @@ +# API 指南:CQRS(start / signal / query)+ 幂等建议 + +SmartEngine 的对外权限集中在 `SmartEngine` 接口,它把服务分为两类: + +- **CommandService**:改变状态(start/signal/abort/complete…) +- **QueryService**:只读查询(find/get/list…) + +代码位置:`core/src/main/java/com/alibaba/smart/framework/engine/SmartEngine.java` + +--- + +## 1. SmartEngine 总入口 + +你在业务系统里通常会把 `SmartEngine` 作为单例注入,然后按需调用其子服务: + +- `getRepositoryCommandService()` +- `getProcessCommandService()` +- `getExecutionCommandService()` +- `getTaskCommandService()` +- `getVariableCommandService()` +- `get*QueryService()` + +## 2. Command API(按服务分组) + +下面根据源码接口自动整理(签名以 dev 分支为准): + +### Command · DeploymentCommandService + +- `DeploymentInstance createDeployment(CreateDeploymentCommand createDeploymentCommand) ;` +- `DeploymentInstance updateDeployment(UpdateDeploymentCommand updateDeploymentCommand) ;` +- `void inactivateDeploymentInstance(String deploymentInstanceId);` +- `void inactivateDeploymentInstance(String deploymentInstanceId,String tenantId);` +- `void activateDeploymentInstance(String deploymentInstanceId);` +- `void activateDeploymentInstance(String deploymentInstanceId,String tenantId);` +- `void deleteDeploymentInstanceLogically(String deploymentInstanceId);` +- `void deleteDeploymentInstanceLogically(String deploymentInstanceId,String tenantId);` + +### Command · ExecutionCommandService + +- `ProcessInstance signal(String executionInstanceId, Map request, Map response);` +- `ProcessInstance signal(String processInstanceId, String executionInstanceId, Map request, Map response);` +- `ProcessInstance signal(String executionInstanceId, Map request);` +- `ProcessInstance signal(String executionInstanceId);` +- `void markDone(String executionInstanceId);` +- `void markDone(String executionInstanceId,String tenantId);` +- `ProcessInstance jumpFrom(ProcessInstance processInstance,String activityId,String executionInstanceId, Map request);` +- `void retry(ProcessInstance processInstance, String activityId, ExecutionContext executionContext);` +- `ExecutionInstance createExecution(ActivityInstance activityInstance);` + +### Command · NotificationCommandService + +- `void markAsRead(String notificationId, String tenantId);` +- `void batchMarkAsRead(List notificationIds, String tenantId);` + +### Command · ProcessCommandService + +- `ProcessInstance start(String processDefinitionId, String processDefinitionVersion, Map request, Map response);` +- `ProcessInstance start(String processDefinitionId, String processDefinitionVersion, Map request);` +- `ProcessInstance start(String processDefinitionId, String processDefinitionVersion);` +- `ProcessInstance start(String processDefinitionId, String processDefinitionVersion,String tenantId);` +- `ProcessInstance startWith(String deploymentInstanceId, String userId, Map request, Map response);` +- `ProcessInstance startWith(String deploymentInstanceId, String userId, Map request);` +- `ProcessInstance startWith(String deploymentInstanceId, Map request);` +- `ProcessInstance startWith(String deploymentInstanceId);` +- `ProcessInstance startWith(String deploymentInstanceId,String tenantId);` +- `void abort(String processInstanceId);` +- `void abort(String processInstanceId,String tenantId);` +- `void abort(String processInstanceId, String reason,String tenantId);` +- `void abort(String processInstanceId, Map request);` + +### Command · RepositoryCommandService + +- `ProcessDefinitionSource deploy(String classPathResource) ;` +- `ProcessDefinitionSource deploy(String classPathResource,String tenantId) ;` +- `ProcessDefinitionSource deploy(InputStream inputStream) ;` +- `ProcessDefinitionSource deploy(InputStream inputStream,String tenantId) ;` +- `ProcessDefinitionSource deployWithUTF8Content(String uTF8ProcessDefinitionContent) ;` +- `ProcessDefinitionSource deployWithUTF8Content(String uTF8ProcessDefinitionContent,String tenantId) ;` + +### Command · SupervisionCommandService + +- `void closeSupervision(String supervisionId, String tenantId);` +- `void autoCloseSupervisionByTask(String taskInstanceId, String tenantId);` + +### Command · TaskCommandService + +- `ProcessInstance complete(String taskId, Map request);` +- `ProcessInstance complete(String taskId, String userId, Map request);` +- `ProcessInstance complete(String taskId, Map request, Map response);` +- `void transfer(String taskId, String fromUserId, String toUserId);` +- `void transfer(String taskId, String fromUserId, String toUserId,String tenantId);` +- `TaskInstance createTask(ExecutionInstance executionInstance, String taskInstanceStatus, Map request);` +- `void markDone(String taskId, Map request);` +- `void removeTaskAssigneeCandidate(String taskId,String tenantId, TaskAssigneeCandidateInstance taskAssigneeCandidateInstance);` +- `void addTaskAssigneeCandidate(String taskId,String tenantId, TaskAssigneeCandidateInstance taskAssigneeCandidateInstance);` +- `void transferWithReason(String taskId, String fromUserId, String toUserId, String reason, String tenantId);` +- `ProcessInstance rollbackTask(String taskId, String targetActivityId, String reason, String tenantId);` +- `void addTaskAssigneeCandidateWithReason(String taskId, String tenantId, TaskAssigneeCandidateInstance taskAssigneeCandidateInstance, String reason);` +- `void removeTaskAssigneeCandidateWithReason(String taskId, String tenantId, TaskAssigneeCandidateInstance taskAssigneeCandidateInstance, String reason);` + +### Command · VariableCommandService + +- `void insert(VariableInstance... variableInstance);` + +### Query · ActivityQueryService + +- `List findAll(String processInstanceId);` +- `List findAll(String processInstanceId,String tenantId);` + +### Query · DeploymentQueryService + +- `DeploymentInstance findById(String deploymentInstanceId);` +- `DeploymentInstance findById(String deploymentInstanceId,String tenantId);` +- `List findList(DeploymentInstanceQueryParam deploymentInstanceQueryParam) ;` +- `Integer count(DeploymentInstanceQueryParam deploymentInstanceQueryParam);` + +### Query · ExecutionQueryService + +- `List findActiveExecutionList(String processInstanceId,String tenantId);` +- `List findActiveExecutionList(String processInstanceId);` +- `List findAll(String processInstanceId);` +- `List findAll(String processInstanceId,String tenantId);` + +### Query · NotificationQueryService + +- `List findNotificationList(NotificationQueryParam param);` +- `Long countNotifications(NotificationQueryParam param);` +- `Long countUnreadNotifications(String receiverUserId, String tenantId);` +- `NotificationInstance findOne(String notificationId, String tenantId);` + +### Query · ProcessQueryService + +- `ProcessInstance findById(String processInstanceId);` +- `ProcessInstance findById(String processInstanceId,String tenantId);` +- `List findList(ProcessInstanceQueryParam processInstanceQueryParam);` +- `Long count(ProcessInstanceQueryParam processInstanceQueryParam);` +- `List findCompletedProcessList(CompletedProcessQueryParam param);` +- `Long countCompletedProcessList(CompletedProcessQueryParam param);` + +### Query · RepositoryQueryService + +- `ProcessDefinition getCachedProcessDefinition(String processDefinitionId, String version);` +- `ProcessDefinition getCachedProcessDefinition(String processDefinitionId, String version,String tenantId);` +- `ProcessDefinition getCachedProcessDefinition(String processDefinitionIdAndVersion);` +- `Collection getAllCachedProcessDefinition();` + +### Query · SupervisionQueryService + +- `List findSupervisionList(SupervisionQueryParam param);` +- `Long countSupervision(SupervisionQueryParam param);` +- `List findActiveSupervisionByTask(String taskInstanceId, String tenantId);` +- `SupervisionInstance findOne(String supervisionId, String tenantId);` + +### Query · TaskAssigneeQueryService + +- `List findList(String taskInstanceId);` +- `List findList(String taskInstanceId,String tenantId);` +- `Map> findAssigneeOfInstanceList(List taskInstanceIdList);` +- `Map> findAssigneeOfInstanceList(List taskInstanceIdList,String tenantId);` + +### Query · TaskQueryService + +- `List findPendingTaskList(PendingTaskQueryParam pendingTaskQueryParam);` +- `Long countPendingTaskList(PendingTaskQueryParam pendingTaskQueryParam);` +- `List findTaskListByAssignee(TaskInstanceQueryByAssigneeParam param);` +- `Long countTaskListByAssignee(TaskInstanceQueryByAssigneeParam param);` +- `List findAllPendingTaskList(String processInstanceId);` +- `List findAllPendingTaskList(String processInstanceId,String tenantId);` +- `TaskInstance findOne(String taskInstanceId);` +- `TaskInstance findOne(String taskInstanceId,String tenantId);` +- `List findList(TaskInstanceQueryParam taskInstanceQueryParam);` +- `Long count(TaskInstanceQueryParam taskInstanceQueryParam);` +- `List findCompletedTaskList(CompletedTaskQueryParam param);` +- `Long countCompletedTaskList(CompletedTaskQueryParam param);` + +### Query · VariableQueryService + +- `List findProcessInstanceVariableList(String processInstanceId);` +- `List findProcessInstanceVariableList(String processInstanceId,String tenantId);` +- `List findList(String processInstanceId, String executionInstanceId);` +- `List findList(String processInstanceId, String executionInstanceId,String tenantId);` + + +> 提示:同一个权限经常存在 tenantId 重载;多租户系统建议统一封装一层 Facade,把 tenantId 的注入与 request special key 的注入都收敛到入口。 + +## 3. Query API(常见查询路径) + +你最常用的查询通常是: + +1) 通过 `ProcessInstance` 查当前运行状态 +2) 通过 `ExecutionInstance` 找 active token(尤其是 receiveTask/并行场景) +3) 通过 `TaskInstance` 查待办与办理记录 +4) 通过 `VariableInstance` 查业务变量(或用于网关条件) + +### 3.1 Fluent Query API(推荐) + +**3.7 版本新增**。对于 Task、ProcessInstance、Supervision、Notification 的查询,推荐使用 Fluent Query API 替代旧版 QueryService: + +```java +// 推荐:Fluent API +List tasks = smartEngine.createTaskQuery() + .taskAssignee(userId) + .taskStatus(status != null, status) // 条件过滤 + .orderByCreateTime().desc() + .listPage(0, 10); + +// 旧版(仍可用,但建议逐步迁移) +TaskInstanceQueryParam param = new TaskInstanceQueryParam(); +param.setClaimUserId(userId); +param.setStatus(status); +List tasks = taskQueryService.findList(param); +``` + +完整文档:[Fluent Query API 使用指南](fluent-query-guide.md) + +### 3.2 旧版 QueryService API 说明 + +以下 QueryService 接口目前**仍然需要**,因为 Fluent API 尚未覆盖: + +- `ExecutionQueryService` — 执行实例查询 +- `ActivityQueryService` — 活动实例查询 +- `DeploymentQueryService` — 部署管理查询 +- `RepositoryQueryService` — 流程定义缓存查询 +- `TaskAssigneeQueryService` — 含 group 关联的任务分配人查询 +- `VariableQueryService` — 流程变量查询 +- `TaskQueryService.findPendingTaskList()` — 含 assignee group 的待办查询 +- `TaskQueryService.findTaskListByAssignee()` — 同上 + +## 4. 幂等建议(生产必做) + +SmartEngine 的 API 设计允许你在上层实现幂等(尤其是 start / signal / complete): + +- **start 幂等**:用业务唯一键(订单号、申请单号)做幂等 key +- **signal 幂等**:用事件 id(messageId)做幂等 key +- **complete 幂等**:用 taskInstanceId + 操作类型 做幂等 key + +实现方式建议: + +- 在你自己的数据库中建立幂等表(key -> result) +- start/signal/complete 前先查幂等表,已执行则直接返回结果 +- 幂等写入与引擎调用尽量放同一事务/同一最终一致性链路 + +> 引擎内部也有一些 request special key 可以承载扩展字段,但幂等更建议由业务层做明确表设计。 + +## 5. 事务边界建议 + +SmartEngine 作为 SDK,**不在内部强制事务控制**,事务边界由调用方决定。这种设计提供了最大的灵活性,但需要调用方正确使用事务。 + +### 5.1 基本原则 + +- **Custom 模式**:推荐把引擎推进与业务写入放在同一事务(你掌控存储接口) +- **DataBase 模式**:推荐用 Spring 事务包裹"引擎调用 + 业务写入",并明确: + - 引擎写入失败时整体回滚 + - 业务写入失败时引擎回滚(避免产生"孤儿流程实例/任务") + +### 5.2 需要事务保护的关键方法 + +以下方法涉及**多个数据库操作**,调用方**必须**在事务上下文中使用,否则可能导致数据不一致: + +| 方法 | 内部操作 | 不加事务的风险 | +|------|---------|---------------| +| `SupervisionCommandService.createSupervision()` | 插入督办记录 → 更新任务优先级 → 发送通知 | 督办创建成功但优先级未更新 | +| `TaskCommandService.complete()` | 标记任务完成 → 关闭督办 → 触发流程执行 | 任务完成但流程未继续 | +| `TaskCommandService.rollbackTask()` | 记录回退 → 执行跳转 | 回退记录存在但流程未回退 | +| `TaskCommandService.transferWithReason()` | 执行转交 → 记录操作 | 转交成功但无记录 | +| `ProcessCommandService.start()` | 创建流程实例 → 创建执行实例 → 创建任务 | 孤儿流程实例 | + +### 5.3 正确用法示例 + +```java +@Service +public class WorkflowFacade { + + @Autowired + private SmartEngine smartEngine; + + /** + * 创建督办 - 必须在事务中调用 + */ + @Transactional(rollbackFor = Exception.class) + public SupervisionInstance createSupervision(String taskId, String supervisorId, + String reason, String type, String tenantId) { + return smartEngine.getSupervisionCommandService() + .createSupervision(taskId, supervisorId, reason, type, tenantId); + } + + /** + * 完成任务 - 必须在事务中调用 + */ + @Transactional(rollbackFor = Exception.class) + public ProcessInstance completeTask(String taskId, Map request) { + // 业务校验 + validateBusinessRules(taskId); + + // 引擎操作(会自动加入当前事务) + ProcessInstance result = smartEngine.getTaskCommandService().complete(taskId, request); + + // 业务后处理 + postProcessCompletion(result); + + return result; + } + + /** + * 组合操作 - 多个引擎调用在同一事务中 + */ + @Transactional(rollbackFor = Exception.class) + public void submitWithSupervision(String taskId, String supervisorId, + Map request) { + // 先创建督办 + smartEngine.getSupervisionCommandService() + .createSupervision(taskId, supervisorId, "紧急处理", "urge", getTenantId()); + + // 再完成任务 + smartEngine.getTaskCommandService().complete(taskId, request); + + // 如果任一步骤失败,整体回滚 + } +} +``` + +### 5.4 事务传播行为 + +SmartEngine 内部的数据库操作会**自动参与调用方的事务**(Spring 事务传播默认是 `REQUIRED`): + +``` +调用方事务 +├── smartEngine.createSupervision() +│ ├── INSERT supervision_instance ← 参与外部事务 +│ ├── UPDATE task_instance ← 参与外部事务 +│ └── INSERT notification_instance ← 参与外部事务 +└── 其他业务操作 +``` + +如果调用方未开启事务,每个数据库操作将**独立提交**,无法保证原子性。 + +### 5.5 Spring 事务配置示例 + +```xml + + + + + + + +``` + +或使用 Java 配置: + +```java +@Configuration +@EnableTransactionManagement +public class TransactionConfig { + + @Bean + public PlatformTransactionManager transactionManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } +} +``` + +详见:`04-persistence/storage-overview.md` + diff --git a/docs/03-usage/fluent-query-guide.md b/docs/03-usage/fluent-query-guide.md new file mode 100644 index 000000000..01e3d0382 --- /dev/null +++ b/docs/03-usage/fluent-query-guide.md @@ -0,0 +1,440 @@ +# Fluent Query API 使用指南 + +SmartEngine 3.7 引入了 Fluent Query API(链式查询 API),借鉴 Activiti/Flowable 的 `Query` 模式,同时吸收了 MyBatis Plus 的条件过滤思想。 + +--- + +## 1. 快速上手 + +### 1.1 基本用法 + +```java +// 获取 SmartEngine 实例 +SmartEngine smartEngine = ...; + +// 查询待办任务 +List tasks = smartEngine.createTaskQuery() + .taskAssignee("user001") + .taskStatus(TaskInstanceConstant.PENDING) + .orderByCreateTime().desc() + .listPage(0, 10); + +// 查询流程实例 +ProcessInstance process = smartEngine.createProcessQuery() + .processInstanceId("12345") + .singleResult(); + +// 统计数量 +long count = smartEngine.createTaskQuery() + .processInstanceId("12345") + .taskStatus(TaskInstanceConstant.PENDING) + .count(); +``` + +### 1.2 可用的查询类型 + +| 入口方法 | 查询接口 | 查询对象 | +|---------|---------|---------| +| `smartEngine.createTaskQuery()` | `TaskQuery` | 任务实例 | +| `smartEngine.createProcessQuery()` | `ProcessInstanceQuery` | 流程实例 | +| `smartEngine.createSupervisionQuery()` | `SupervisionQuery` | 督办实例 | +| `smartEngine.createNotificationQuery()` | `NotificationQuery` | 通知实例 | + +### 1.3 终端操作 + +每个查询必须以终端操作结尾: + +| 方法 | 说明 | +|------|------| +| `list()` | 返回全部匹配结果 | +| `listPage(offset, limit)` | 分页查询 | +| `singleResult()` | 返回唯一结果(多个结果时抛异常) | +| `count()` | 返回匹配数量 | + +--- + +## 2. 条件过滤 + +### 2.1 TaskQuery 完整过滤条件 + +```java +smartEngine.createTaskQuery() + // === 主键与关联 === + .taskInstanceId("task001") // 任务实例 ID + .processInstanceId("proc001") // 流程实例 ID + .processInstanceIdIn(idList) // 多个流程实例 ID + .activityInstanceId("act001") // 活动实例 ID + + // === 业务属性 === + .taskAssignee("user001") // 任务处理人 + .taskStatus("pending") // 任务状态(单值) + .taskStatusIn("pending", "running") // 任务状态(多值,OR 关系) + .taskPriority(500) // 优先级 + .taskTitle("审批") // 标题 + .taskTag("urgent") // 标签 + .taskComment("备注") // 备注 + .taskExtension("ext_data") // 扩展字段 + + // === 流程定义 === + .processDefinitionType("approval") // 流程定义类型 + .processDefinitionActivityId("userTask1") // 流程定义活动 ID + + // === 时间范围 === + .completeTimeAfter(startDate) // 完成时间 >= startDate + .completeTimeBefore(endDate) // 完成时间 < endDate + + // === 分页与租户 === + .tenantId("tenant001") // 租户 ID + .pageOffset(0).pageSize(10) // 分页 + + // === 排序(可多字段) === + .orderByPriority().desc() + .orderByCreateTime().asc() + + // === 终端操作 === + .list(); +``` + +### 2.2 ProcessInstanceQuery 完整过滤条件 + +```java +smartEngine.createProcessQuery() + .processInstanceId("proc001") + .processInstanceIdIn(idList) + .startedBy("user001") // 发起人 + .processStatus("running") // 流程状态 + .processDefinitionType("approval") + .processDefinitionIdAndVersion("myProcess:1") + .parentInstanceId("parentProc001") // 父流程实例 + .bizUniqueId("ORDER-2026-001") // 业务唯一键 + .startedAfter(startDate) // 发起时间 >= startDate + .startedBefore(endDate) // 发起时间 < endDate + .completedAfter(completeStart) // 完成时间 >= completeStart + .completedBefore(completeEnd) // 完成时间 < completeEnd + .orderByStartTime().desc() + .orderByCompleteTime().asc() + .listPage(0, 20); +``` + +### 2.3 SupervisionQuery 完整过滤条件 + +```java +smartEngine.createSupervisionQuery() + .supervisionId("sup001") + .supervisorUserId("supervisor001") // 督办人 + .taskInstanceId("task001") // 关联任务 + .taskInstanceIdIn(taskIdList) + .processInstanceId("proc001") + .processInstanceIdIn(procIdList) + .supervisionType("urge") // 督办类型 + .supervisionStatus("active") // 督办状态 + .supervisionTimeAfter(startDate) + .supervisionTimeBefore(endDate) + .orderByCreateTime().desc() + .listPage(0, 10); +``` + +### 2.4 NotificationQuery 完整过滤条件 + +```java +smartEngine.createNotificationQuery() + .notificationId("notif001") + .senderUserId("sender001") // 发送人 + .receiverUserId("receiver001") // 接收人 + .taskInstanceId("task001") + .taskInstanceIdIn(taskIdList) + .processInstanceId("proc001") + .processInstanceIdIn(procIdList) + .notificationType("task_assign") // 通知类型 + .readStatus("unread") // 已读状态 + .notificationTimeAfter(startDate) + .notificationTimeBefore(endDate) + .orderByCreateTime().desc() + .orderByReadTime().asc() + .listPage(0, 20); +``` + +--- + +## 3. 条件过滤(动态条件) + +借鉴 MyBatis Plus 的 `eq(condition, value)` 模式,每个过滤方法都提供 `(boolean condition, value)` 重载。当 `condition = false` 时,该条件不会被添加到查询中。 + +### 3.1 使用场景 + +在实际业务中,查询条件通常来自前端表单,有些字段可能为空: + +```java +// ❌ 传统方式:大量 if-else +TaskQuery query = smartEngine.createTaskQuery(); +if (userId != null) { + query.taskAssignee(userId); +} +if (status != null) { + query.taskStatus(status); +} +if (processType != null) { + query.processDefinitionType(processType); +} +List tasks = query.list(); + +// ✅ 条件过滤:一行搞定 +List tasks = smartEngine.createTaskQuery() + .taskAssignee(userId != null, userId) + .taskStatus(status != null, status) + .processDefinitionType(processType != null, processType) + .orderByCreateTime().desc() + .listPage(0, 10); +``` + +### 3.2 可用的条件重载方法 + +**Query(基础):** +- `tenantId(boolean condition, String tenantId)` +- `pageOffset(boolean condition, int offset)` +- `pageSize(boolean condition, int size)` + +**ProcessBoundQuery(TaskQuery / SupervisionQuery / NotificationQuery 共享):** +- `processInstanceId(boolean condition, String processInstanceId)` + +**TaskQuery:** +- `taskAssignee(boolean condition, String claimUserId)` +- `taskStatus(boolean condition, String status)` +- `processDefinitionType(boolean condition, String type)` +- `taskTag(boolean condition, String tag)` +- `taskTitle(boolean condition, String title)` + +**ProcessInstanceQuery:** +- `processInstanceId(boolean condition, String processInstanceId)` +- `startedBy(boolean condition, String startUserId)` +- `processStatus(boolean condition, String status)` +- `processDefinitionType(boolean condition, String type)` +- `bizUniqueId(boolean condition, String bizUniqueId)` + +**SupervisionQuery:** +- `supervisorUserId(boolean condition, String supervisorUserId)` +- `supervisionType(boolean condition, String type)` +- `supervisionStatus(boolean condition, String status)` + +**NotificationQuery:** +- `receiverUserId(boolean condition, String receiverUserId)` +- `notificationType(boolean condition, String type)` +- `readStatus(boolean condition, String readStatus)` + +--- + +## 4. 多值条件 + +### 4.1 taskStatusIn + +同时查询多种状态的任务: + +```java +// 查询所有"待处理"和"处理中"的任务 +List tasks = smartEngine.createTaskQuery() + .taskStatusIn("pending", "running") + .orderByCreateTime().desc() + .list(); + +// 或使用 List +List statuses = Arrays.asList("pending", "running", "suspended"); +List tasks = smartEngine.createTaskQuery() + .taskStatusIn(statuses) + .list(); +``` + +> **注意**:`taskStatusIn` 与 `taskStatus` 互斥。同时设置时,`taskStatusIn` 优先生效。 + +### 4.2 processInstanceIdIn + +```java +// 批量查询多个流程实例的任务 +List processIds = Arrays.asList("proc001", "proc002", "proc003"); +List tasks = smartEngine.createTaskQuery() + .processInstanceIdIn(processIds) + .list(); +``` + +--- + +## 5. 排序 + +### 5.1 单字段排序 + +```java +// 按创建时间倒序 +smartEngine.createTaskQuery() + .orderByCreateTime().desc() + .list(); +``` + +### 5.2 多字段排序 + +排序条件按调用顺序生效(先调用的优先级更高): + +```java +// 先按优先级降序,再按创建时间升序 +smartEngine.createTaskQuery() + .orderByPriority().desc() + .orderByCreateTime().asc() + .list(); +``` + +### 5.3 各查询类型支持的排序字段 + +| TaskQuery | ProcessInstanceQuery | SupervisionQuery | NotificationQuery | +|-----------|---------------------|-----------------|-------------------| +| `orderByTaskId()` | `orderByProcessInstanceId()` | `orderBySupervisionId()` | `orderByNotificationId()` | +| `orderByCreateTime()` | `orderByStartTime()` | `orderByCreateTime()` | `orderByCreateTime()` | +| `orderByModifyTime()` | `orderByModifyTime()` | `orderByModifyTime()` | `orderByModifyTime()` | +| `orderByClaimTime()` | `orderByCompleteTime()` | `orderByCloseTime()` | `orderByReadTime()` | +| `orderByCompleteTime()` | | | | +| `orderByPriority()` | | | | + +--- + +## 6. 接口继承结构 + +``` +Query ← 基础:分页、租户、终端操作 +├── ProcessBoundQuery ← 共享:processInstanceId、排序 +│ ├── TaskQuery ← 任务:assignee、status、priority... +│ ├── SupervisionQuery ← 督办:supervisor、type... +│ └── NotificationQuery ← 通知:sender、receiver、readStatus... +└── ProcessInstanceQuery ← 流程:startUser、bizUniqueId、parentInstance... +``` + +`ProcessBoundQuery` 是一个共享接口(借鉴 Flowable 的 `TaskInfoQuery` 设计),将 `processInstanceId`、`orderByCreateTime`、`orderByModifyTime`、`asc`、`desc` 等公共方法提取到父接口,减少重复。 + +--- + +## 7. 典型业务场景示例 + +### 7.1 待办任务列表页(带搜索) + +```java +public PageResult getMyTodoList(String userId, String keyword, + String processType, int page, int size) { + TaskQuery query = smartEngine.createTaskQuery() + .taskAssignee(userId) + .taskStatus(TaskInstanceConstant.PENDING) + .processDefinitionType(processType != null, processType) + .taskTitle(keyword != null, keyword) + .tenantId(getTenantId()) + .orderByPriority().desc() + .orderByCreateTime().desc(); + + long total = query.count(); + List list = query.listPage(page * size, size); + + return new PageResult<>(list, total); +} +``` + +### 7.2 我发起的流程列表 + +```java +public List getMyStartedProcesses(String userId, String status) { + return smartEngine.createProcessQuery() + .startedBy(userId) + .processStatus(status != null, status) + .tenantId(getTenantId()) + .orderByStartTime().desc() + .listPage(0, 20); +} +``` + +### 7.3 未读通知数 + 通知列表 + +```java +// 统计未读通知数 +long unreadCount = smartEngine.createNotificationQuery() + .receiverUserId(currentUserId) + .readStatus("unread") + .tenantId(getTenantId()) + .count(); + +// 获取通知列表 +List notifications = smartEngine.createNotificationQuery() + .receiverUserId(currentUserId) + .readStatus(filterReadStatus != null, filterReadStatus) + .tenantId(getTenantId()) + .orderByCreateTime().desc() + .listPage(0, 20); +``` + +### 7.4 查询某流程的督办记录 + +```java +List supervisions = smartEngine.createSupervisionQuery() + .processInstanceId("proc001") + .supervisionStatus("active") + .tenantId(getTenantId()) + .orderByCreateTime().desc() + .list(); +``` + +--- + +## 8. 与旧版 QueryService API 的关系 + +### 8.1 共存策略 + +Fluent Query API 是旧版 `XxxQueryService` + `XxxQueryParam` 的**上层封装**,两套 API 可以共存使用。 + +### 8.2 迁移映射 + +| 旧 API | Fluent API 等价写法 | +|--------|-------------------| +| `taskQueryService.findList(param)` | `smartEngine.createTaskQuery()...list()` | +| `taskQueryService.count(param)` | `smartEngine.createTaskQuery()...count()` | +| `processQueryService.findList(param)` | `smartEngine.createProcessQuery()...list()` | +| `processQueryService.findById(id)` | `smartEngine.createProcessQuery().processInstanceId(id).singleResult()` | +| `taskQueryService.findOne(taskId)` | `smartEngine.createTaskQuery().taskInstanceId(taskId).singleResult()` | +| `taskQueryService.findAllPendingTaskList(procId)` | `smartEngine.createTaskQuery().processInstanceId(procId).taskStatus("pending").list()` | +| `supervisionQueryService.findSupervisionList(param)` | `smartEngine.createSupervisionQuery()...list()` | +| `notificationQueryService.findNotificationList(param)` | `smartEngine.createNotificationQuery()...list()` | + +### 8.3 尚未覆盖的旧 API + +以下旧 API 目前没有直接的 Fluent 等价物,建议继续使用旧 API: + +| 旧 API | 原因 | +|--------|------| +| `ExecutionQueryService` | 执行实例查询较底层,使用频率低 | +| `ActivityQueryService` | 活动实例查询较底层 | +| `DeploymentQueryService` | 部署管理查询 | +| `RepositoryQueryService` | 缓存查询,不走数据库 | +| `TaskAssigneeQueryService` | 任务分配人查询(含 group 逻辑) | +| `VariableQueryService` | 变量查询 | +| `findPendingTaskList(PendingTaskQueryParam)` | 含 assignee group 关联查询,Fluent API 暂不支持 | +| `findTaskListByAssignee(TaskInstanceQueryByAssigneeParam)` | 同上 | +| `findCompletedTaskList(CompletedTaskQueryParam)` | 含 participantUserId 关联查询 | + +--- + +## 9. 与 Activiti/Flowable 的对比 + +### 9.1 设计差异 + +| 维度 | Activiti/Flowable | SmartEngine | +|------|-------------------|-------------| +| 运行/历史分离 | 两套表 + 两套 Query | 同表通过 status 区分 | +| 查询类型数量 | 10+ 种 | 4 种(Task/Process/Supervision/Notification) | +| 条件动态应用 | 无(需手动 if-else) | ✅ `(boolean condition, value)` 重载 | +| 督办/通知查询 | 无 | ✅ 独有 | +| OR 条件 | `or()/endOr()` | 暂不支持 | +| 变量值过滤 | `variableValueEquals(name, value)` | 暂不支持 | + +### 9.2 覆盖度 + +在轻量引擎定位下,SmartEngine 的 Fluent API 覆盖了 Activiti/Flowable 约 **60-65%** 的实用过滤方法。主要缺失项(后续迭代补充): + +| 缺失方法 | 优先级 | 场景 | +|---------|:------:|------| +| `taskTitleLike` / `taskNameLike` | P0 | 模糊搜索 | +| `taskCreatedBefore/After` | P0 | 按创建时间过滤 | +| `taskUnassigned()` | P1 | 待认领任务池 | +| `processDefinitionTypeIn(List)` | P1 | 多流程类型过滤 | +| `or()/endOr()` | P2 | 复杂条件组合 | +| `variableValueEquals()` | P2 | 变量值过滤 | diff --git a/docs/03-usage/jump-and-advanced.md b/docs/03-usage/jump-and-advanced.md new file mode 100644 index 000000000..7799c3b18 --- /dev/null +++ b/docs/03-usage/jump-and-advanced.md @@ -0,0 +1,98 @@ +# 跳转、会签、分派、VariablePersister 等高级特性 + +本章聚焦“生产系统一定会遇到,但不是入门必需”的权限: + +- jumpTo / jumpFrom:流程跳转(人工干预、补偿、迁移) +- markDone:标记完成 +- userTask 分派:候选人/候选组/组织/岗位/会签 +- VariablePersister:变量持久化策略 +- Retry:失败重试扩展 + +--- + +## 1. jumpTo / jumpFrom:流程跳转 + +入口在 `ExecutionCommandService`(签名见 `03-usage/api-guide.md`)。 + +典型用法: + +- **跳到某个节点**:线上修复,跳过失败节点 +- **从某个节点跳出**:跳过一段流程,直接进入下游 + +强烈建议: + +- 跳转前先冻结流程实例(suspend)或做并发控制 +- 跳转行为本质是“改变执行图”,请记录审计(建议写入你自己的操作日志表) +- 跳转后要处理: + - 未完成的任务如何关闭 + - 变量如何清理/补齐 + +仓库示例:`storage-custom/src/test/java/.../jump/*` + +--- + +## 2. markDone:人工标记完成 + +用于: + +- 外部系统已经完成动作,但引擎推进卡住 +- 需要人工强制结束某分支 + +注意: + +- markDone 与 jump 一样需要审计 +- DataBase 模式下要同步处理任务表/分派表状态 + +--- + +## 3. userTask 分派与会签(DataBase 模式) + +### 3.1 分派入口:TaskAssigneeDispatcher + +推荐你把所有“谁可以办这个任务”的规则收敛到一个实现类里: + +- candidateUser / candidateGroup +- org / position +- 动态规则(按变量、按角色、按业务字段) + +然后在 engine 配置中注入: + +```java +cfg.setTaskAssigneeDispatcher(new YourTaskAssigneeDispatcher()); +``` + +### 3.2 会签(multi-instance) + +仓库存在 multi-instance 相关测试资源,建议你先跑通并理解: + +- 会签是产生多个 taskInstance,还是一个 task + 多个 assignee? +- 通过条件/计数何时结束会签? + +--- + +## 4. VariablePersister:控制变量写库 + +你可以通过自定义 `VariablePersister`: + +- 控制哪些变量落库 +- 控制序列化格式(JSON/JSONB/加密) +- 控制变量清理策略(只保留最新、只保留白名单) + +--- + +## 5. Retry:失败重试 + +SmartEngine 提供: + +- `ExceptionProcessor`:统一异常入口 +- `@Retryable`:声明重试策略 +- retry 扩展模块:`extension/retry/*`(含 DB 存储实现与 schema) + +生产建议: + +- 重试要可观测(日志/指标/告警) +- 重试要可停止(熔断、最大次数) +- 对外部系统调用做幂等(避免重复扣款/重复发货) + +详见:`06-ops/failure-handling.md` + diff --git a/docs/03-usage/modeling-guide.md b/docs/03-usage/modeling-guide.md new file mode 100644 index 000000000..c19f72843 --- /dev/null +++ b/docs/03-usage/modeling-guide.md @@ -0,0 +1,100 @@ +# 建模指南:网关 / 并行 / join / receiveTask / userTask + +SmartEngine 支持的 BPMN 子集与执行语义偏“工程落地”,建议你在建模时遵循以下规则,避免上线后出现: + +- 并行 join 卡死 +- ReceiveTask 无法推进 +- userTask 任务分派不一致 +- 变量作用域混乱 + +--- + +## 1. 基础规则(强烈建议) + +### 1.1 每个流程必须具备可追踪的唯一标识 + +- processId(BPMN process id)建议稳定、可版本化 +- 建议把“业务幂等 key / bizUniqueId”作为变量或扩展字段,贯穿 start/signal + +### 1.2 把“业务执行”放在 ServiceTask,把“等待外部事件”放在 ReceiveTask + +- ServiceTask:同步调用你的业务逻辑(委派/脚本/规则) +- ReceiveTask:等待外部 signal(回调、消息、人工确认、异步补偿) + +不要在 ServiceTask 里“自旋等待外部事件”。 + +--- + +## 2. 网关与并发 + +### 2.1 ExclusiveGateway(互斥网关) + +- 条件表达式应当是纯函数(只依赖变量) +- 避免在条件里调用外部系统(会导致不可重放、不可测试) + +### 2.2 ParallelGateway(并行网关) + +并行网关会造成: + +- execution 数量增加 +- join 需要等待全部分支到达 + +建议: + +- 分支数量可控(避免“数据驱动产生大量分支”) +- 每个分支尽量短小,避免长事务 +- 在 DataBase 模式下确保索引完备(见 `04-persistence/database-schema.md`) + +并发专题:`05-extensibility/concurrency.md` + +### 2.3 InclusiveGateway(包容网关) + +- 包容网关 join 更复杂:不是“所有分支”,而是“实际激活的分支” +- 建议你先跑仓库对应测试并理解树结构(`gateway/tree/*`) + +--- + +## 3. ReceiveTask 的正确用法 + +### 3.1 何时会进入等待状态? + +当 execution 推进到 ReceiveTask: + +- activityInstance 进入等待状态 +- executionInstance 通常处于 active 但无法继续推进 + +### 3.2 你需要怎么 signal? + +- 你必须保存 “要 signal 哪个 execution / process” 的关联信息 +- 推荐做法: + - 把 executionInstanceId 存入你的业务表(或事件 payload) + - 外部事件到达后用 `ExecutionCommandService.signal(executionInstanceId, request)` 推进 + +--- + +## 4. userTask 建模(DataBase 模式) + +userTask 的关键不在 BPMN,而在“分派规则与组织模型”: + +- candidateUser / candidateGroup / org / position 等分派语义 +- 认领/完成/转派/会签等操作语义 + +建议: + +- 把分派逻辑收敛到 `TaskAssigneeDispatcher`(统一入口) +- userTask 的业务扩展字段通过 request special key 传入(见 `RequestMapSpecialKeyConstant`) + +详见:`05-extensibility/user-integration.md` + +--- + +## 5. 流程跳转与人工干预(慎用) + +建模阶段尽量避免“依赖跳转才能跑通的流程”。跳转通常用于: + +- 线上故障补偿 +- 人工干预修复 +- 迁移/升级时的临时手段 + +详见:`03-usage/jump-and-advanced.md` + diff --git a/docs/03-usage/variables.md b/docs/03-usage/variables.md new file mode 100644 index 000000000..e2bec8243 --- /dev/null +++ b/docs/03-usage/variables.md @@ -0,0 +1,64 @@ +# 变量与上下文:作用域、序列化、持久化策略 + +SmartEngine 的变量体系主要用于: + +- 业务数据在流程节点间流转 +- 网关条件表达式求值 +- 外部 signal 携带上下文推进流程 + +本章说明变量如何进入引擎、如何持久化、以及你需要注意的“特殊 key”。 + +--- + +## 1. 变量从哪里来? + +常见入口有三类: + +1) start 时传入 `request` map +2) signal 时传入 `request` map +3) 任务完成/认领等操作的 request map + +这些 map 会进入 `ExecutionContext`,并由 `VariableCommandService`/`VariablePersister` 决定是否持久化。 + +## 2. 变量的持久化与序列化 + +- `VariablePersister`:决定把哪些变量写入 `se_variable_instance` +- 默认实现:`DefaultVariablePersister` + +建议: + +- 只持久化“必要变量”(用于恢复推进、用于条件判断、用于审计) +- 大对象(复杂 JSON)建议存引用(如对象存储 key),避免表膨胀 +- 统一序列化策略(JSON / JSONB)并在 MyBatis typeHandler 中固定 + +## 3. 作用域(Scope) + +SmartEngine 的变量作用域通常至少区分: + +- processInstance 级(全局) +- execution/activity 级(分支/节点局部) +- task 级(userTask 表单数据) + +具体字段与实现请参考 `VariableInstance` 与 `VariableCommandService`。 + +## 4. RequestMapSpecialKeyConstant(非常重要) + +仓库定义了一组“特殊 key”,用于传递: + +- tenantId +- task 相关扩展字段(如候选人、组、组织等) +- 以及一些引擎内部控制字段 + +代码位置:`core/.../constant/RequestMapSpecialKeyConstant.java` + +建议你的系统在入口层做: + +- 白名单化:只允许特定 key 进入引擎 +- 命名空间隔离:业务变量不要使用 `_$_smart_engine_$_` 前缀 + +## 5. 常见坑 + +- **同名变量覆盖**:不同节点写同名 key,会覆盖旧值(注意作用域) +- **类型不一致**:同一 key 有时是 String、有时是 Long,会导致网关条件判断异常、或 MyBatis 写库异常 +- **PostgreSQL 类型坑**:如果 variable 表列是 jsonb/bytea,typeHandler 要严格匹配(见 `04-persistence/mybatis-notes.md`) + diff --git a/docs/04-persistence/database-schema.md b/docs/04-persistence/database-schema.md new file mode 100644 index 000000000..082422f8d --- /dev/null +++ b/docs/04-persistence/database-schema.md @@ -0,0 +1,602 @@ +# DataBase 模式表结构/索引/清理策略 + +本章包含 3 部分: + +1) SmartEngine 核心表(process/execution/activity/task/variable/deployment) +2) 工作流增强表(通知/督办/转派/回退等) +3) 索引与清理策略建议 + +--- + +## 1. 核心表(DDL) + +### 1.1 MySQL 版(schema.sql) + +```sql + + +CREATE TABLE `se_deployment_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK' , + `gmt_create` datetime(6) NOT NULL COMMENT 'create time' , + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time' , + `process_definition_id` varchar(255) NOT NULL COMMENT 'process definition id' , + `process_definition_version` varchar(255) DEFAULT NULL COMMENT 'process definition version' , + `process_definition_type` varchar(255) DEFAULT NULL COMMENT 'process definition type' , + `process_definition_code` varchar(255) DEFAULT NULL COMMENT 'process definition code' , + `process_definition_name` varchar(255) DEFAULT NULL COMMENT 'process definition name' , + `process_definition_desc` varchar(255) DEFAULT NULL COMMENT 'process definition desc' , + `process_definition_content` mediumtext NOT NULL COMMENT 'process definition content' , + `deployment_user_id` varchar(128) NOT NULL COMMENT 'deployment user id' , + `deployment_status` varchar(64) NOT NULL COMMENT 'deployment status' , + `logic_status` varchar(64) NOT NULL COMMENT 'logic status' , + `extension` mediumtext DEFAULT NULL COMMENT 'extension' , + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id' , + + PRIMARY KEY (`id`) +) ; + +CREATE TABLE `se_process_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK' , + `gmt_create` datetime(6) NOT NULL COMMENT 'create time' , + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time' , + `process_definition_id_and_version` varchar(128) NOT NULL COMMENT 'process definition id and version' , + `process_definition_type` varchar(255) DEFAULT NULL COMMENT 'process definition type' , + `status` varchar(64) NOT NULL COMMENT ' 1.running 2.completed 3.aborted', + `parent_process_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'parent process instance id' , + `parent_execution_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'parent execution instance id' , + `start_user_id` varchar(128) DEFAULT NULL COMMENT 'start user id' , + `biz_unique_id` varchar(255) DEFAULT NULL COMMENT 'biz unique id' , + `reason` varchar(255) DEFAULT NULL COMMENT 'reason' , + `comment` varchar(255) DEFAULT NULL COMMENT 'comment' , + `title` varchar(255) DEFAULT NULL COMMENT 'title' , + `tag` varchar(255) DEFAULT NULL COMMENT 'tag' , + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id' , + + PRIMARY KEY (`id`) +) ; + +CREATE TABLE `se_activity_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK' , + `gmt_create` datetime(6) NOT NULL COMMENT 'create time' , + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time' , + `process_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'process instance id' , + `process_definition_id_and_version` varchar(255) NOT NULL COMMENT 'process definition id and version' , + `process_definition_activity_id` varchar(64) NOT NULL COMMENT 'process definition activity id' , + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id' , + PRIMARY KEY (`id`) +) ; + +CREATE TABLE `se_task_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK' , + `gmt_create` datetime(6) NOT NULL COMMENT 'create time' , + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time' , + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id' , + `process_definition_id_and_version` varchar(128) DEFAULT NULL COMMENT 'process definition id and version' , + `process_definition_type` varchar(255) DEFAULT NULL COMMENT 'process definition type' , + `activity_instance_id` bigint(20) unsigned NOT NULL COMMENT 'activity instance id' , + `process_definition_activity_id` varchar(255) NOT NULL COMMENT 'process definition activity id' , + `execution_instance_id` bigint(20) unsigned NOT NULL COMMENT 'execution instance id' , + `claim_user_id` varchar(255) DEFAULT NULL COMMENT 'claim user id' , + `title` varchar(255) DEFAULT NULL COMMENT 'title' , + `priority` int(11) DEFAULT 500 COMMENT 'priority' , + `tag` varchar(255) DEFAULT NULL COMMENT 'tag' , + `claim_time` datetime(6) DEFAULT NULL COMMENT 'claim time' , + `complete_time` datetime(6) DEFAULT NULL COMMENT 'complete time' , + `status` varchar(255) NOT NULL COMMENT 'status' , + `comment` varchar(255) DEFAULT NULL COMMENT 'comment' , + `extension` varchar(255) DEFAULT NULL COMMENT 'extension' , + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id' , + + PRIMARY KEY (`id`) +) ; + +CREATE TABLE `se_execution_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK' , + `gmt_create` datetime(6) NOT NULL COMMENT 'create time' , + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time' , + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id' , + `process_definition_id_and_version` varchar(255) NOT NULL COMMENT 'process definition id and version' , + `process_definition_activity_id` varchar(255) NOT NULL COMMENT 'process definition activity id' , + `activity_instance_id` bigint(20) unsigned NOT NULL COMMENT 'activity instance id' , + `block_id` bigint(20) unsigned DEFAULT NULL COMMENT 'block_id' , + `active` tinyint(4) NOT NULL COMMENT '1:active 0:inactive', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id' , + PRIMARY KEY (`id`) +) ; + + +CREATE TABLE `se_task_assignee_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK' , + `gmt_create` datetime(6) NOT NULL COMMENT 'create time' , + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time' , + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id' , + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id' , + `assignee_id` varchar(255) NOT NULL COMMENT 'assignee id' , + `assignee_type` varchar(128) NOT NULL COMMENT 'assignee type' , + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id' , + PRIMARY KEY (`id`) +) ; + + +CREATE TABLE `se_variable_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK' , + `gmt_create` datetime(6) NOT NULL COMMENT 'create time' , + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time' , + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id' , + `execution_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'execution instance id' , + `field_key` varchar(128) NOT NULL COMMENT 'field key' , + `field_type` varchar(128) NOT NULL COMMENT 'field type' , + `field_double_value` decimal(65,30) DEFAULT NULL COMMENT 'field double value' , + `field_long_value` bigint(20) DEFAULT NULL COMMENT 'field long value' , + `field_string_value` varchar(4000) DEFAULT NULL COMMENT 'field string value' , + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id' , + + PRIMARY KEY (`id`) +) ; +``` + + +### 1.2 PostgreSQL 版(schema-postgre.sql) + +```sql +CREATE TABLE se_deployment_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_definition_id varchar(255) NOT NULL, + process_definition_version varchar(255), + process_definition_type varchar(255), + process_definition_code varchar(255), + process_definition_name varchar(255), + process_definition_desc varchar(255), + process_definition_content text NOT NULL, + deployment_user_id varchar(128) NOT NULL, + deployment_status varchar(64) NOT NULL, + logic_status varchar(64) NOT NULL, + extension text, + tenant_id varchar(64), + PRIMARY KEY (id) +); + +CREATE TABLE se_process_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_definition_id_and_version varchar(128) NOT NULL, + process_definition_type varchar(255), + status varchar(64) NOT NULL, + parent_process_instance_id bigint, + parent_execution_instance_id bigint, + start_user_id varchar(128), + biz_unique_id varchar(255), + reason varchar(255), + comment varchar(255), + title varchar(255), + tag varchar(255), + tenant_id varchar(64), + PRIMARY KEY (id) +); + +CREATE TABLE se_activity_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint, + process_definition_id_and_version varchar(255) NOT NULL, + process_definition_activity_id varchar(64) NOT NULL, + tenant_id varchar(64), + PRIMARY KEY (id) +); + +CREATE TABLE se_task_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + process_definition_id_and_version varchar(128), + process_definition_type varchar(255), + activity_instance_id bigint NOT NULL, + process_definition_activity_id varchar(255) NOT NULL, + execution_instance_id bigint NOT NULL, + claim_user_id varchar(255), + title varchar(255), + priority int DEFAULT 500, + tag varchar(255), + claim_time timestamp(6), + complete_time timestamp(6), + status varchar(255) NOT NULL, + comment varchar(255), + extension varchar(255), + tenant_id varchar(64), + PRIMARY KEY (id) +); + +CREATE TABLE se_execution_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + process_definition_id_and_version varchar(255) NOT NULL, + process_definition_activity_id varchar(255) NOT NULL, + activity_instance_id bigint NOT NULL, + block_id bigint, + active smallint NOT NULL, + tenant_id varchar(64), + PRIMARY KEY (id) +); + +CREATE TABLE se_task_assignee_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint NOT NULL, + assignee_id varchar(255) NOT NULL, + assignee_type varchar(128) NOT NULL, + tenant_id varchar(64), + PRIMARY KEY (id) +); + +CREATE TABLE se_variable_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + execution_instance_id bigint, + field_key varchar(128) NOT NULL, + field_type varchar(128) NOT NULL, + field_double_value decimal(65,30), + field_long_value bigint, + field_string_value varchar(4000), + tenant_id varchar(64), + PRIMARY KEY (id) +); +``` + + +--- + +## 2. 工作流增强表(DDL) + +### 2.1 MySQL(workflow-enhancement-schema-mysql.sql) + +```sql +-- 工作流管理系统增强功能数据库表结构 +-- 基于SmartEngine现有表结构设计规范 + +-- 督办记录表 +CREATE TABLE `se_supervision_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `supervisor_user_id` varchar(255) NOT NULL COMMENT 'supervisor user id', + `supervision_reason` varchar(500) DEFAULT NULL COMMENT 'supervision reason', + `supervision_type` varchar(64) NOT NULL COMMENT 'supervision type: urge/track/remind', + `status` varchar(64) NOT NULL COMMENT 'status: active/closed', + `close_time` datetime(6) DEFAULT NULL COMMENT 'close time', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`), + KEY `idx_process_instance_id` (`process_instance_id`), + KEY `idx_task_instance_id` (`task_instance_id`), + KEY `idx_supervisor_user_id` (`supervisor_user_id`), + KEY `idx_status` (`status`), + KEY `idx_tenant_id` (`tenant_id`) +); + +-- 知会抄送表 +CREATE TABLE `se_notification_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'task instance id', + `sender_user_id` varchar(255) NOT NULL COMMENT 'sender user id', + `receiver_user_id` varchar(255) NOT NULL COMMENT 'receiver user id', + `notification_type` varchar(64) NOT NULL COMMENT 'notification type: cc/inform', + `title` varchar(255) DEFAULT NULL COMMENT 'notification title', + `content` varchar(1000) DEFAULT NULL COMMENT 'notification content', + `read_status` varchar(64) NOT NULL DEFAULT 'unread' COMMENT 'read status: unread/read', + `read_time` datetime(6) DEFAULT NULL COMMENT 'read time', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`), + KEY `idx_process_instance_id` (`process_instance_id`), + KEY `idx_task_instance_id` (`task_instance_id`), + KEY `idx_sender_user_id` (`sender_user_id`), + KEY `idx_receiver_user_id` (`receiver_user_id`), + KEY `idx_read_status` (`read_status`), + KEY `idx_tenant_id` (`tenant_id`) +); + +-- 任务移交记录表 +CREATE TABLE `se_task_transfer_record` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `from_user_id` varchar(255) NOT NULL COMMENT 'from user id', + `to_user_id` varchar(255) NOT NULL COMMENT 'to user id', + `transfer_reason` varchar(500) DEFAULT NULL COMMENT 'transfer reason', + `deadline` datetime(6) DEFAULT NULL COMMENT 'processing deadline', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`), + KEY `idx_task_instance_id` (`task_instance_id`), + KEY `idx_from_user_id` (`from_user_id`), + KEY `idx_to_user_id` (`to_user_id`), + KEY `idx_tenant_id` (`tenant_id`) +); + +-- 加签减签操作记录表 +CREATE TABLE `se_assignee_operation_record` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `operation_type` varchar(64) NOT NULL COMMENT 'operation type: add_assignee/remove_assignee', + `operator_user_id` varchar(255) NOT NULL COMMENT 'operator user id', + `target_user_id` varchar(255) NOT NULL COMMENT 'target user id', + `operation_reason` varchar(500) DEFAULT NULL COMMENT 'operation reason', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`), + KEY `idx_task_instance_id` (`task_instance_id`), + KEY `idx_operation_type` (`operation_type`), + KEY `idx_operator_user_id` (`operator_user_id`), + KEY `idx_target_user_id` (`target_user_id`), + KEY `idx_tenant_id` (`tenant_id`) +); + +-- 流程回退记录表 +CREATE TABLE `se_process_rollback_record` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `rollback_type` varchar(64) NOT NULL COMMENT 'rollback type: previous/specific', + `from_activity_id` varchar(255) NOT NULL COMMENT 'from activity id', + `to_activity_id` varchar(255) NOT NULL COMMENT 'to activity id', + `operator_user_id` varchar(255) NOT NULL COMMENT 'operator user id', + `rollback_reason` varchar(500) DEFAULT NULL COMMENT 'rollback reason', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`), + KEY `idx_process_instance_id` (`process_instance_id`), + KEY `idx_task_instance_id` (`task_instance_id`), + KEY `idx_rollback_type` (`rollback_type`), + KEY `idx_operator_user_id` (`operator_user_id`), + KEY `idx_tenant_id` (`tenant_id`) +); +``` + + +### 2.2 PostgreSQL(workflow-enhancement-schema-postgresql.sql) + +```sql +CREATE TABLE se_supervision_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint NOT NULL, + supervisor_user_id varchar(255) NOT NULL, + supervision_reason varchar(500), + supervision_type varchar(64) NOT NULL, + status varchar(64) NOT NULL, + close_time timestamp(6), + tenant_id varchar(64) +); + +CREATE INDEX idx_supervision_process_instance_id + ON se_supervision_instance (process_instance_id); + +CREATE INDEX idx_supervision_task_instance_id + ON se_supervision_instance (task_instance_id); + +CREATE INDEX idx_supervision_supervisor_user_id + ON se_supervision_instance (supervisor_user_id); + +CREATE INDEX idx_supervision_status + ON se_supervision_instance (status); + +CREATE INDEX idx_supervision_tenant_id + ON se_supervision_instance (tenant_id); + +COMMENT ON TABLE se_supervision_instance IS '督办记录表'; +COMMENT ON COLUMN se_supervision_instance.supervision_type IS 'urge/track/remind'; +COMMENT ON COLUMN se_supervision_instance.status IS 'active/closed'; + + + +CREATE TABLE se_notification_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint, + sender_user_id varchar(255) NOT NULL, + receiver_user_id varchar(255) NOT NULL, + notification_type varchar(64) NOT NULL, + title varchar(255), + content varchar(1000), + read_status varchar(64) NOT NULL DEFAULT 'unread', + read_time timestamp(6), + tenant_id varchar(64) +); + +CREATE INDEX idx_notification_process_instance_id + ON se_notification_instance (process_instance_id); + +CREATE INDEX idx_notification_task_instance_id + ON se_notification_instance (task_instance_id); + +CREATE INDEX idx_notification_sender_user_id + ON se_notification_instance (sender_user_id); + +CREATE INDEX idx_notification_receiver_user_id + ON se_notification_instance (receiver_user_id); + +CREATE INDEX idx_notification_read_status + ON se_notification_instance (read_status); + +CREATE INDEX idx_notification_tenant_id + ON se_notification_instance (tenant_id); + +COMMENT ON TABLE se_notification_instance IS '知会/抄送记录表'; +COMMENT ON COLUMN se_notification_instance.notification_type IS 'cc/inform'; +COMMENT ON COLUMN se_notification_instance.read_status IS 'unread/read'; + +CREATE TABLE se_assignee_operation_record ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + task_instance_id bigint NOT NULL, + operation_type varchar(64) NOT NULL, + operator_user_id varchar(255) NOT NULL, + target_user_id varchar(255) NOT NULL, + operation_reason varchar(500), + tenant_id varchar(64) +); + +CREATE INDEX idx_assignee_op_task_instance_id + ON se_assignee_operation_record (task_instance_id); + +CREATE INDEX idx_assignee_op_operation_type + ON se_assignee_operation_record (operation_type); + +CREATE INDEX idx_assignee_op_operator_user_id + ON se_assignee_operation_record (operator_user_id); + +CREATE INDEX idx_assignee_op_target_user_id + ON se_assignee_operation_record (target_user_id); + +CREATE INDEX idx_assignee_op_tenant_id + ON se_assignee_operation_record (tenant_id); + +COMMENT ON TABLE se_assignee_operation_record IS '加签/减签操作记录表'; +COMMENT ON COLUMN se_assignee_operation_record.operation_type + IS 'add_assignee/remove_assignee'; + + + + +CREATE TABLE se_task_transfer_record ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + task_instance_id bigint NOT NULL, + from_user_id varchar(255) NOT NULL, + to_user_id varchar(255) NOT NULL, + transfer_reason varchar(500), + deadline timestamp(6), + tenant_id varchar(64) +); + +CREATE INDEX idx_task_transfer_task_instance_id + ON se_task_transfer_record (task_instance_id); + +CREATE INDEX idx_task_transfer_from_user_id + ON se_task_transfer_record (from_user_id); + +CREATE INDEX idx_task_transfer_to_user_id + ON se_task_transfer_record (to_user_id); + +CREATE INDEX idx_task_transfer_tenant_id + ON se_task_transfer_record (tenant_id); + +COMMENT ON TABLE se_task_transfer_record IS '任务移交记录表'; + + +CREATE TABLE se_process_rollback_record ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint NOT NULL, + rollback_type varchar(64) NOT NULL, + from_activity_id varchar(255) NOT NULL, + to_activity_id varchar(255) NOT NULL, + operator_user_id varchar(255) NOT NULL, + rollback_reason varchar(500), + tenant_id varchar(64) +); + +CREATE INDEX idx_rollback_process_instance_id + ON se_process_rollback_record (process_instance_id); + +CREATE INDEX idx_rollback_task_instance_id + ON se_process_rollback_record (task_instance_id); + +CREATE INDEX idx_rollback_type + ON se_process_rollback_record (rollback_type); + +CREATE INDEX idx_rollback_operator_user_id + ON se_process_rollback_record (operator_user_id); + +CREATE INDEX idx_rollback_tenant_id + ON se_process_rollback_record (tenant_id); + +COMMENT ON TABLE se_process_rollback_record IS '流程回退记录表'; +COMMENT ON COLUMN se_process_rollback_record.rollback_type + IS 'previous/specific'; +``` + + +--- + +## 3. 索引(index.sql) + +```sql + + +alter table `se_execution_instance` + add key `idx_tenant_id_process_instance_id_and_status_tenant_id` (tenant_id,process_instance_id,active), + add key `idx_tenant_id_process_instance_id_and_activity_instance_id` (tenant_id,process_instance_id,activity_instance_id); + +alter table `se_task_assignee_instance` + add key `idx_tenant_id_task_instance_id` (tenant_id,task_instance_id), + add key `idx_tenant_id_assignee_id_and_type` (tenant_id,assignee_id,assignee_type); + +alter table `se_activity_instance` + add key `idx_tenant_id_process_instance_id` (tenant_id,process_instance_id); + +alter table `se_deployment_instance` + add key `idx_tenant_id_user_id_and_logic_status` (tenant_id,logic_status,deployment_user_id); + +alter table `se_process_instance` + add key `idx_tenant_id_start_user_id` (tenant_id,start_user_id), + add key `idx_tenant_id_status` (tenant_id,status); + +alter table `se_task_instance` + add key `idx_tenant_id_status` (tenant_id,status), + add key `idx_tenant_id_process_instance_id_and_status` (tenant_id,process_instance_id,status), + add key `idx_tenant_id_process_definition_type` (tenant_id,process_definition_type), + add key `idx_tenant_id_process_instance_id` (tenant_id,process_instance_id), + add key `idx_tenant_id_claim_user_id` (tenant_id,claim_user_id), + add key `idx_tenant_id_tag` (tenant_id,tag), + add key `idx_tenant_id_activity_instance_id` (tenant_id,activity_instance_id), + add key `idx_tenant_id_process_definition_activity_id` (tenant_id,process_definition_activity_id); + +alter table `se_variable_instance` + add key `idx_tenant_id_process_instance_id_and_execution_instance_id` (tenant_id,process_instance_id,execution_instance_id); +``` + + +--- + +## 4. 清理策略(建议) + +SmartEngine 默认表结构没有单独的“历史表”体系,常见做法是: + +- 使用 `status` 字段区分 active/closed(或 deleted) +- 定期清理: + - 已完成且超过保留期的 process/execution/activity/task/variable + - 已关闭的通知/督办/转派/回退记录 + +建议你在业务系统侧明确: + +- 保留期(如 90/180/365 天) +- 清理批大小(避免大事务) +- 清理顺序(先 task/assignee/variable,再 activity/execution,再 process/deployment) + +如果你需要更完善的审计/回放权限,建议自建 history 表或引入事件日志(event sourcing),并把引擎推进过程输出为可重放事件。 + diff --git a/docs/04-persistence/multi-database.md b/docs/04-persistence/multi-database.md new file mode 100644 index 000000000..6d0642314 --- /dev/null +++ b/docs/04-persistence/multi-database.md @@ -0,0 +1,176 @@ +# 多数据库支持指南 + +SmartEngine 通过 Dialect(方言)体系支持多种数据库,包括国产信创数据库。 + +--- + +## 1. 支持的数据库 + +| 数据库 | 族群 | 方言实现 | Schema 脚本 | 状态 | +|--------|------|---------|------------|------| +| MySQL 8.0+ | MySQL 系 | `MySqlDialect` | `schema.sql` | ✅ 生产就绪 | +| MariaDB | MySQL 系 | `MySqlDialect` | `schema.sql` | ✅ 复用 MySQL | +| OceanBase | MySQL 系 | `OceanBaseDialect` | `schema.sql` | ✅ 复用 MySQL | +| PostgreSQL 12+ | PG 系 | `PostgreSqlDialect` | `schema-postgre.sql` | ✅ 生产就绪 | +| 人大金仓 KingBase | PG 系 | `KingbaseDialect` | `schema-postgre.sql` | ✅ 复用 PostgreSQL | +| Oracle 12c+ | Oracle 系 | `OracleDialect` | `schema-oracle.sql` | ✅ 生产就绪 | +| 达梦 DM 8.0+ | Oracle 系 | `DmDialect` | `schema-oracle.sql` | ✅ 复用 Oracle | +| SQL Server 2016+ | 独立 | `SqlServerDialect` | 需自行适配 | ⚠️ 方言可用 | +| H2 | 测试 | `H2Dialect` | `schema-h2-only-for-test.sql` | ✅ 仅测试 | + +### 1.1 族群策略 + +不需要为每种数据库维护独立的 DDL 脚本。3 套核心脚本覆盖全部: + +``` +MySQL 系:MySQL, MariaDB, OceanBase, TiDB, GBase + → schema.sql + +PG 系: PostgreSQL, 人大金仓(KingBase), CockroachDB + → schema-postgre.sql + +Oracle 系:Oracle, 达梦(DM) + → schema-oracle.sql +``` + +--- + +## 2. SQL 脚本清单 + +脚本位置:`extension/storage/storage-mysql/src/main/resources/sql/` + +### 2.1 基础表(7 张) + +| 脚本 | MySQL | PostgreSQL | Oracle/达梦 | H2 | +|------|-------|-----------|------------|-----| +| 基础表 | `schema.sql` | `schema-postgre.sql` | `schema-oracle.sql` | `schema-h2-only-for-test.sql` | +| 工作流增强表 | `workflow-enhancement-schema-mysql.sql` | `workflow-enhancement-schema-postgresql.sql` | `workflow-enhancement-schema-oracle.sql` | — | +| 索引 | `index.sql` | 内含于增强 schema | `index-oracle.sql` | — | + +### 2.2 迁移脚本 + +``` +sql/migration/ +├── V001__add_process_complete_time.sql # MySQL +├── V001__add_process_complete_time_postgresql.sql # PostgreSQL +├── V001__add_process_complete_time_oracle.sql # Oracle/达梦 +├── V1.1__optimize_indexes.sql # MySQL +└── V1.1__optimize_indexes_postgresql.sql # PostgreSQL +``` + +--- + +## 3. 类型映射 + +| 语义 | MySQL | PostgreSQL | Oracle/达梦 | H2 | +|------|-------|-----------|-----------|-----| +| 主键 | `bigint AUTO_INCREMENT` | `bigint GENERATED BY DEFAULT AS IDENTITY` | `NUMBER(19) DEFAULT seq.NEXTVAL` | `bigint AUTO_INCREMENT` | +| 时间戳 | `datetime(6)` | `timestamp(6)` | `TIMESTAMP(6)` | `datetime(6)` | +| 大文本 | `mediumtext` | `text` | `CLOB` | `text` | +| 短字符串 | `varchar(N)` | `varchar(N)` | `VARCHAR2(N)` | `varchar(N)` | +| 布尔/标志 | `tinyint(4)` | `smallint` | `NUMBER(1)` | `tinyint` | +| 整数 | `int(11)` | `int` | `NUMBER(10)` | `int` | +| 高精度 | `decimal(65,30)` | `decimal(65,30)` | `NUMBER(65,30)` | `decimal(65,30)` | + +--- + +## 4. Dialect 方言体系 + +### 4.1 自动检测 + +方言通过 JDBC URL 自动检测,无需手动配置: + +```java +// 自动检测示例 +// jdbc:mysql://... → MySqlDialect +// jdbc:postgresql://... → PostgreSqlDialect +// jdbc:oracle:thin://... → OracleDialect +// jdbc:dm://... → DmDialect +// jdbc:kingbase://... → KingbaseDialect +``` + +### 4.2 手动指定 + +```java +DialectRegistry.getInstance().getDialect("mysql"); // 按名称 +DialectRegistry.getInstance().detectDialect(jdbcUrl); // 按 URL +``` + +### 4.3 Dialect 接口能力 + +| 能力 | 方法 | 说明 | +|------|------|------| +| 分页 | `buildPageSql(sql, offset, limit)` | LIMIT OFFSET / OFFSET FETCH | +| 时间 | `getCurrentTimestamp()` | 当前时间戳表达式 | +| 日期差 | `getDateDiff(start, end)` | 跨库日期差计算 | +| ID 生成 | `getIdGenerationType()` | AUTO_INCREMENT / IDENTITY / SEQUENCE | +| 数据类型 | `getBigintType()`, `getClobType()` ... | 类型映射 | +| 字符串 | `concat(...)`, `substring(...)` | 字符串函数 | +| 锁 | `getForUpdateClause()` | 悲观锁语法 | +| 标识符 | `quoteIdentifier(name)` | 表名列名引用 | +| 特性检测 | `supportsFeature(feature)` | CTE, JSON, UPSERT 等 | + +### 4.4 扩展自定义方言 + +```java +public class MyDialect extends AbstractDialect { + public MyDialect() { + super("mydb", "My Database"); + } + + @Override + public String buildPageSql(String sql, int offset, int limit) { + return sql + " LIMIT " + limit + " OFFSET " + offset; + } + // ... 实现其他方法 +} + +// 注册 +DialectRegistry.getInstance().registerDialect(new MyDialect()); +``` + +--- + +## 5. 部署指南 + +### 5.1 MySQL 部署 + +```bash +# 1. 创建数据库 +mysql -u root -e "CREATE DATABASE smart_engine DEFAULT CHARSET utf8mb4;" + +# 2. 执行基础表 +mysql -u root smart_engine < sql/schema.sql + +# 3. 执行工作流增强表(如需要) +mysql -u root smart_engine < sql/workflow-enhancement-schema-mysql.sql + +# 4. 执行索引 +mysql -u root smart_engine < sql/index.sql + +# 5. 执行迁移 +mysql -u root smart_engine < sql/migration/V001__add_process_complete_time.sql +``` + +### 5.2 PostgreSQL 部署 + +```bash +psql -U postgres -c "CREATE DATABASE smart_engine;" +psql -U postgres -d smart_engine -f sql/schema-postgre.sql +psql -U postgres -d smart_engine -f sql/workflow-enhancement-schema-postgresql.sql +psql -U postgres -d smart_engine -f sql/migration/V001__add_process_complete_time_postgresql.sql +psql -U postgres -d smart_engine -f sql/migration/V1.1__optimize_indexes_postgresql.sql +``` + +### 5.3 Oracle / 达梦部署 + +```bash +# Oracle +sqlplus user/password@dbname @sql/schema-oracle.sql +sqlplus user/password@dbname @sql/workflow-enhancement-schema-oracle.sql +sqlplus user/password@dbname @sql/index-oracle.sql +sqlplus user/password@dbname @sql/migration/V001__add_process_complete_time_oracle.sql + +# 达梦(语法兼容 Oracle) +disql user/password@host:port @sql/schema-oracle.sql +``` diff --git a/docs/04-persistence/mybatis-notes.md b/docs/04-persistence/mybatis-notes.md new file mode 100644 index 000000000..256ec6d5e --- /dev/null +++ b/docs/04-persistence/mybatis-notes.md @@ -0,0 +1,91 @@ +# MyBatis SQL 兼容、类型坑、分页/复杂查询扩展建议 + +DataBase 模式使用 MyBatis(SQLMap)实现持久化,常见问题集中在: + +- MySQL ↔ PostgreSQL 的类型差异(bigint/varchar/boolean/json/jsonb) +- 动态 SQL 的参数类型推断 +- in (...) 参数展开与 JDBC 类型 +- 分页语法(limit/offset)与复杂查询扩展 + +SQLMap 位置:`extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/` + +当前 SQLMap 文件: + +- `activity_instance.xml` +- `deployment_instance.xml` +- `execution_instance.xml` +- `notification_instance.xml` +- `process_instance.xml` +- `supervision_instance.xml` +- `task_assignee_instance.xml` +- `task_instance.xml` +- `variable_instance.xml` + +--- + +## 1. PostgreSQL 最常见报错:operator does not exist + +典型错误(你在迁移时很容易遇到): + +- `bigint = character varying` +- `smallint = boolean` + +根因: + +- MyBatis 绑定参数时按 Java 类型推断 JDBC 类型 +- PostgreSQL 对类型更严格(不会像 MySQL 那样隐式转换) + +解决建议(优先级从高到低): + +1) **保证 Java 参数类型与列类型一致**(最佳) +2) 在 XML 中显式指定 `jdbcType`(例如 BIGINT / VARCHAR / BOOLEAN) +3) 必要时在 SQL 中显式 cast(PostgreSQL `::bigint` / `CAST(... AS bigint)`) +4) 为 json/jsonb/枚举等使用自定义 `TypeHandler` + +> 你之前遇到的 `id in (?)` + `bigint = varchar`,本质也是同类问题:in 参数被当成 String 绑定了。 + +--- + +## 2. boolean / smallint 映射 + +仓库 PostgreSQL DDL 中,部分状态字段使用 `smallint`(0/1),而 Java 可能用 Boolean。 + +建议: + +- 状态字段统一使用 Integer/Short(并用枚举封装) +- 或使用 TypeHandler 显式映射(Boolean ↔ smallint) + +--- + +## 3. in (...) 参数与集合绑定 + +如果你需要 `WHERE id IN (...)`: + +- 用 MyBatis `` 展开集合 +- 确保集合元素类型与列类型一致(Long 列就传 Long 集合) + +--- + +## 4. 分页与复杂查询扩展 + +仓库的 QueryParam 多为简单字段过滤。生产常见扩展: + +- 多维度待办过滤:人员/组/组织/岗位/优先级/标签/时间区间… +- 组合条件与排序(优先级 + 更新时间 + SLA) +- 按业务字段 join(例如按订单号联查) + +建议做法: + +- 不要把所有查询都塞进引擎模块,保留“引擎最小查询集” +- 复杂查询在业务系统侧自建 read model(CQRS 的 Query 侧) + - 例如把 task_instance 与 assignee_instance join 后落到一张 denormalized 表 + - 或用 ES/ClickHouse 做全文与聚合 + +--- + +## 5. SQLMap 维护建议 + +- 为每个 SQLMap 文件建立对应的“回归测试用例” +- PostgreSQL 与 MySQL 需要“双跑”(CI 中分别跑) +- 关键 SQL(待办列表、并行网关查询)做 explain analyze 定期评估 + diff --git a/docs/04-persistence/storage-overview.md b/docs/04-persistence/storage-overview.md new file mode 100644 index 000000000..23d336821 --- /dev/null +++ b/docs/04-persistence/storage-overview.md @@ -0,0 +1,94 @@ +# 存储分层:接口、扩展点、事务边界 + +SmartEngine 的存储设计核心思想是: + +- 引擎核心只依赖 **Storage 接口** +- 不同落地模式(Custom / DataBase)通过扩展模块提供实现 + +--- + +## 1. Storage 接口清单(核心) + +接口位置:`core/src/main/java/com/alibaba/smart/framework/engine/instance/storage/` + +- `ActivityInstanceStorage` +- `DeploymentInstanceStorage` +- `ExecutionHistoryInstanceStorage`(注意:仓库默认 DDL 未提供 history 表,通常由你扩展实现) +- `ExecutionInstanceStorage` +- `NotificationInstanceStorage` +- `ProcessDefinitionStorage`(流程定义存储抽象,Custom/DataBase 各自有策略) +- `ProcessInstanceStorage` +- `SupervisionInstanceStorage` +- `TaskAssigneeInstanceStorage` +- `TaskInstanceStorage` +- `VariableInstanceStorage` + +> 这些接口的入参里经常包含 `ProcessEngineConfiguration`,用于拿到 tenantId、idGenerator、instanceAccessor、variablePersister 等上下文权限。 + +--- + +## 2. DataBase 模式的实现层次 + +DataBase 模式模块:`extension/storage/storage-mysql` + +典型实现结构: + +- `persister/database/entity/*`:MyBatis entity +- `persister/database/dao/*`:DAO 接口 +- `mybatis/sqlmap/*.xml`:SQLMap +- `persister/database/service/RelationshipDatabase*Storage`:Storage 接口实现(包装 DAO,提供事务边界) + +--- + +## 3. Custom 模式的实现层次 + +Custom 模式模块:`extension/storage/storage-custom` + +它的实现基于: + +- `PersisterSession`:线程内 session(Map 存储) +- `Custom*Storage`:实现 Storage 接口 +- 单元测试用例用它来验证引擎语义(并行网关、跳转等) + +生产环境建议: + +- 以 Storage 接口为契约,实现你自己的数据库/事件日志/缓存存储 +- 明确事务边界:引擎推进与业务写入的一致性策略(强一致 or 最终一致) + +--- + +## 4. 事务边界(建议) + +### 4.1 Custom 模式(推荐强一致) + +如果你的 Storage 直接对接业务数据库: + +- 引擎推进(写 process/execution/activity/task/variable)与业务写入放同一事务 +- start/signal/complete 等外部入口做幂等(见 `03-usage/api-guide.md`) + +### 4.2 DataBase 模式(Spring Tx) + +典型做法: + +- 入口方法(Controller/Service)加 `@Transactional` +- 业务写入 + 引擎调用都在同一事务中 +- 失败回滚:避免引擎产生“孤儿实例/孤儿任务” + +--- + +## 5. 表结构策略(TableSchemaStrategy) + +引擎配置里存在 `TableSchemaStrategy`(默认 `DefaultTableSchemaStrategy`),用于: + +- 决定表名前缀、schema、或多租户策略(例如 schema-per-tenant) +- 影响 MyBatis/DAO 的 SQL 拼接(实现细节以 storage-mysql 为准) + +如果你要实现“同一套引擎同时支持 MySQL/PostgreSQL、多租户 schema 隔离”,这里是关键扩展点之一。 + +--- + +下一步: + +- DataBase 表结构与索引:`database-schema.md` +- MyBatis 兼容与类型坑:`mybatis-notes.md` + diff --git a/docs/05-extensibility/behaviors.md b/docs/05-extensibility/behaviors.md new file mode 100644 index 000000000..b20c48058 --- /dev/null +++ b/docs/05-extensibility/behaviors.md @@ -0,0 +1,66 @@ +# 节点行为扩展:ServiceTask / ReceiveTask / 网关… + +SmartEngine 的执行核心抽象是 `ActivityBehavior`: + +- 每种 BPMN 节点类型对应一个 behavior +- behavior 可以通过扩展机制替换/覆盖 + +--- + +## 1. 内置行为在哪? + +- BPMN 行为(事件、网关、sequence flow 等):`core/.../engine/bpmn/behavior/*` +- Task 行为(ServiceTask/ReceiveTask/UserTask 等):`core/.../engine/behavior/impl/*` + +--- + +## 2. 如何覆盖某类节点行为? + +### 2.1 使用 ExtensionBinding 绑定 + +- group:`ExtensionConstant.ACTIVITY_BEHAVIOR` +- bindKey:节点类型(例如 serviceTask) + +实现类示意: + +```java +@ExtensionBinding(group = ExtensionConstant.ACTIVITY_BEHAVIOR, bindKey = "serviceTask", priority = 100) +public class YourServiceTaskBehavior extends ServiceTaskBehavior { + @Override + public void execute(...) { ... } +} +``` + +### 2.2 你应该优先“组合”而不是“重写全部” + +常见推荐做法: + +- 在执行前后加埋点、权限校验、幂等校验 +- 委派调用仍复用默认 DelegationExecutor +- 异常仍交给 ExceptionProcessor + +--- + +## 3. Delegation(业务委派)怎么对接? + +ServiceTask 等节点通常最终会走 `DelegationExecutor`: + +- 解析阶段:从 smart:class / 其他扩展属性得到 class/beanName +- 执行阶段:通过 InstanceAccessor 获取实例并调用 + +你可以替换: + +- `InstanceAccessor`(决定如何拿实例) +- `DelegationExecutor`(决定如何调用、是否异步、是否熔断) + +--- + +## 4. 网关行为扩展注意事项 + +- Exclusive/Inclusive/Parallel 的语义复杂,建议尽量不要“重写语义” +- 如果你确实要扩展: + - 建议只在“条件表达式求值、并发控制、线程池选择”层面做增强 + - 并配套跑通仓库 gateway/tree 的测试 + +并发专题见:`05-extensibility/concurrency.md` + diff --git a/docs/05-extensibility/concurrency.md b/docs/05-extensibility/concurrency.md new file mode 100644 index 000000000..7a4f99842 --- /dev/null +++ b/docs/05-extensibility/concurrency.md @@ -0,0 +1,68 @@ +# 并发:ParallelGateway 的 LockStrategy / ExecutorService + +并行网关是 SmartEngine 最“工程化”的部分之一:它既是 BPMN 语义,又会在生产里遇到真实的并发问题。 + +本章解释: + +- 并行分叉/汇聚时执行实例如何变化 +- 引擎如何选择线程池、如何等待、如何避免重复推进 +- 你应该如何在集群/多线程下配置与扩展 + +--- + +## 1. ParallelGatewayUtil:并行的“配置解释器” + +关键类:`core/.../util/ParallelGatewayUtil.java` + +它负责从节点 properties 中解析: + +- 使用哪个 ExecutorService(默认或指定 poolName) +- latch 的等待时间 +- 并发相关的开关与参数 + +意味着:你可以在 BPMN 中通过扩展属性,为不同并行网关指定不同的线程池策略。 + +--- + +## 2. ExecutorService poolsMap:多线程池治理 + +`ProcessEngineConfiguration` 中存在: + +- `Map getExecutorServiceMap()`(或同等配置项) + +常见策略: + +- 默认线程池:用于大多数并行网关 +- 专用线程池:用于“重任务”分支(外部调用、IO、耗时计算) +- 限流:通过线程池大小/队列大小控制系统背压 + +--- + +## 3. LockStrategy(Deprecated)还能用吗? + +`LockStrategy` 接口在源码中标记为 Deprecated,原因通常是: + +- 锁粒度太细或语义不清 +- 在集群环境下无法保证正确性(需要分布式锁/事务锁) + +建议: + +- 不再依赖 LockStrategy 做核心正确性 +- 采用数据库层面的行锁/乐观锁(DataBase 模式) +- 或采用“幂等推进 + 去重”(Custom 模式) + +--- + +## 4. 生产建议(最重要) + +- 并行网关 join 的正确性优先于性能 +- 任何并行推进都要有幂等保障(避免重复执行业务 side effect) +- 外部调用建议: + - 幂等 + 重试 + 熔断 + - 失败可补偿(compensation) + +仓库里并行相关的测试非常多,建议你把它们当作“语义回归集”: + +- storage-custom:ConcurrentParallelGatewayTest 等 +- storage-mysql:ConcurrentParallelGatewayDBTest 等 + diff --git a/docs/05-extensibility/overview.md b/docs/05-extensibility/overview.md new file mode 100644 index 000000000..b65c741a6 --- /dev/null +++ b/docs/05-extensibility/overview.md @@ -0,0 +1,110 @@ +# 扩展点总览:parser / behavior / storage / user integration / lock / executor… + +SmartEngine 的二次开发权限主要来自: + +1) **ExtensionBinding(注解扩展)**:按 group + bindKey 绑定实现 +2) **配置注入(ProcessEngineConfiguration)**:把关键组件换成你的实现 +3) **Storage 接口**:替换持久化实现(Custom/DataBase/混合) + +--- + +## 1. ExtensionBinding:如何“挂载”扩展实现 + +注解:`core/.../engine/extension/annotation/ExtensionBinding.java` + +核心字段: + +- `group`:扩展类别(见 `ExtensionConstant`) +- `bindKey`:绑定 key(通常是 BPMN 元素类型、属性名、或行为名) +- `priority`:同一 bindKey 下的优先级(越大越优先) + +扩展分组常量见:`core/.../engine/extension/constant/ExtensionConstant.java` + +- `element-parser` +- `attribute-parser` +- `activity-behavior` +- 以及 `common` / `SERVICE` + +引擎启动时会通过 `AnnotationScanner` 扫描 classpath,构建 ExtensionContainer(扩展容器)。 + +--- + +## 2. 最常用的“配置注入”扩展点 + +这些扩展点不走 ExtensionBinding,而是直接在配置里注入实现: + +- `SmartEngine getSmartEngine();` +- `void setSmartEngine(SmartEngine smartEngine);` +- `void setIdGenerator(IdGenerator idGenerator);` +- `IdGenerator getIdGenerator();` +- `void setInstanceAccessor(InstanceAccessor instanceAccessor);` +- `InstanceAccessor getInstanceAccessor();` +- `void setExpressionEvaluator(ExpressionEvaluator expressionEvaluator);` +- `ExpressionEvaluator getExpressionEvaluator();` +- `void setDelegationExecutor(DelegationExecutor delegationExecutor);` +- `DelegationExecutor getDelegationExecutor();` +- `void setListenerExecutor(ListenerExecutor listenerExecutor);` +- `ListenerExecutor getListenerExecutor();` +- `void setAnnotationScanner(AnnotationScanner annotationScanner);` +- `AnnotationScanner getAnnotationScanner();` +- `void setExceptionProcessor(ExceptionProcessor exceptionProcessor);` +- `ExceptionProcessor getExceptionProcessor();` +- `void setParallelServiceOrchestration(ParallelServiceOrchestration parallelServiceOrchestration);` +- `ParallelServiceOrchestration getParallelServiceOrchestration();` +- `void setTaskAssigneeDispatcher(TaskAssigneeDispatcher taskAssigneeDispatcher);` +- `TaskAssigneeDispatcher getTaskAssigneeDispatcher();` +- `void setVariablePersister(VariablePersister variablePersister);` +- `VariablePersister getVariablePersister();` +- `void setMultiInstanceCounter(MultiInstanceCounter multiInstanceCounter);` +- `MultiInstanceCounter getMultiInstanceCounter();` +- `void setLockStrategy(LockStrategy lockStrategy);` +- `LockStrategy getLockStrategy();` +- `void setTableSchemaStrategy(TableSchemaStrategy tableSchemaStrategy);` +- `TableSchemaStrategy getTableSchemaStrategy();` +- `void setExecutorService(ExecutorService executorService);` +- `ExecutorService getExecutorService();` +- `void setExecutorServiceMap(Map poolsMap);` +- `Map getExecutorServiceMap();` +- `void setOptionContainer(OptionContainer optionContainer);` +- `OptionContainer getOptionContainer();` +- `void setMagicExtension(Map extension);` +- `Map getMagicExtension();` +- `void setPvmActivityTaskFactory(PvmActivityTaskFactory pvmActivityTaskFactory);` +- `PvmActivityTaskFactory getPvmActivityTaskFactory();` + +> 建议阅读 `DefaultProcessEngineConfiguration`,它展示了“引擎默认怎么组装这些权限”。 + +--- + +## 3. 典型二开路径(按需求分类) + +### 3.1 你想支持更多 BPMN 扩展属性/元素 + +- 实现 ElementParser / AttributeParser +- 绑定到 `element-parser` / `attribute-parser` +- 参考:`05-extensibility/parser.md` + +### 3.2 你想改变某类节点的执行逻辑(例如 ServiceTask) + +- 实现/继承 `ActivityBehavior` +- 绑定到 `activity-behavior` +- 参考:`05-extensibility/behaviors.md` + +### 3.3 你想替换存储(换 DB、加历史表、做归档) + +- 实现 Storage 接口(`core/.../instance/storage/*`) +- 替换 DataBase 模式的 RelationshipDatabase*Storage +- 参考:`05-extensibility/storage.md` + +### 3.4 你想把 user/组织/岗位/待办融入你的 IAM/RBAC + +- 实现 `TaskAssigneeDispatcher` +- 配置 `InstanceAccessor` 与 `TaskCommandService` 的调用策略 +- 参考:`05-extensibility/user-integration.md` + +### 3.5 你想加强并行/异步执行权限 + +- 配置 `ParallelServiceOrchestration` / ExecutorService poolsMap +- 理解 `ParallelGatewayUtil` 的属性语义 +- 参考:`05-extensibility/concurrency.md` + diff --git a/docs/05-extensibility/parser.md b/docs/05-extensibility/parser.md new file mode 100644 index 000000000..bf72b6f72 --- /dev/null +++ b/docs/05-extensibility/parser.md @@ -0,0 +1,62 @@ +# BPMN 解析扩展(ElementParser / AttributeParser) + +SmartEngine 在解析 BPMN 时会: + +1) 解析标准 BPMN 元素(process/startEvent/serviceTask/gateway…) +2) 解析扩展属性与扩展元素(smart:*、camunda:* 等) +3) 将结果写入 ProcessDefinition / 节点 properties,为执行阶段提供依据 + +扩展点主要有两类: + +- ElementParser:解析某类 BPMN 元素 +- AttributeParser:解析某个属性/扩展属性 + +它们都通过 `@ExtensionBinding` 挂载。 + +--- + +## 1. ElementParser + +绑定组:`ExtensionConstant.ELEMENT_PARSER` + +典型 bindKey: + +- 元素类型(serviceTask、receiveTask、parallelGateway…) +- 或你自定义的扩展元素名 + +实现建议: + +- 解析阶段只做“结构化提取”,不要做业务 side effect +- 把扩展结果写入节点 properties(或 extension map) +- 为每个 parser 写单元测试(参考 core 的 parser test) + +--- + +## 2. AttributeParser + +绑定组:`ExtensionConstant.ATTRIBUTE_PARSER` + +典型 bindKey: + +- `smart:class` +- `camunda:class` +- 以及各种 property / listener / expression + +实现建议: + +- 统一做命名空间兼容(建议复用 magicExtension 的映射) +- 明确解析优先级(priority),避免被其他实现覆盖 + +--- + +## 3. 参考用例(仓库) + +- 命名空间兼容测试:`core/src/test/java/.../ExtensionNameSpaceParseTest` +- 扩展元素解析测试:`core/src/test/resources/process-def/extend/extend.bpmn20.xml` + +如果你要支持更多 BPMN 特性(例如 boundary event、timer、event subprocess),建议: + +- 先把解析与执行行为拆开 +- parser 只负责把结构抽取出来 +- behavior 再消费 parser 产物 + diff --git a/docs/05-extensibility/storage.md b/docs/05-extensibility/storage.md new file mode 100644 index 000000000..72d2a7720 --- /dev/null +++ b/docs/05-extensibility/storage.md @@ -0,0 +1,64 @@ +# 自定义存储实现:实例/变量/任务/活动 + +如果你要把 SmartEngine 深度嵌入你的业务系统,最常见的二开需求就是: + +- 用你自己的数据库(或多租户 schema) +- 增加业务字段 +- 增加历史表/归档表 +- 做读写分离/复杂查询 + +这些都通过 Storage 接口完成。 + +--- + +## 1. Storage 接口在哪里? + +`core/.../instance/storage/*` + +每个接口对应一个“领域对象族”的持久化: + +- Process / Execution / Activity +- Task / Assignee +- Variable +- Deployment / Definition +- Notification / Supervision(增强) + +--- + +## 2. 参考实现:DataBase 模式 + +模块:`extension/storage/storage-mysql` + +实现特点: + +- RelationshipDatabase*Storage 作为 Storage 层 +- DAO + SQLMap 作为数据访问层 +- schema.sql / schema-postgre.sql 作为 DDL + +如果你要适配 PostgreSQL/MySQL 双栈: + +- 建议把“SQL 方言差异”收敛到 MyBatis provider 或专用 SQLMap +- 并为关键查询(待办、并发 join)做双库回归测试 + +--- + +## 3. 参考实现:Custom 模式 + +模块:`extension/storage/storage-custom` + +实现特点: + +- PersisterSession(线程内)做存根 +- 主要用于测试与理解接口 + + +--- + +## 4. 设计建议(生产) + +- 写入路径:尽量少的表更新、明确索引 +- 查询路径:不要依赖“引擎核心表”支撑所有复杂查询 + - 复杂待办建议做 read model(CQRS Query 侧) +- 数据保留:没有 history 表时,建议自己补齐“历史事件/操作日志” +- 多租户:建议把 tenantId 作为一级过滤条件,并建立复合索引(tenant_id + status + gmt_modified 等) + diff --git a/docs/05-extensibility/user-integration.md b/docs/05-extensibility/user-integration.md new file mode 100644 index 000000000..8255a6f6b --- /dev/null +++ b/docs/05-extensibility/user-integration.md @@ -0,0 +1,78 @@ +# 用户/组织/待办分派(DataBase 模式) + +DataBase 模式提供了 userTask 的待办表结构,但“谁可以办”必须与你的组织/IAM 模型对齐。 + +SmartEngine 提供两个关键扩展点: + +- InstanceAccessor:如何拿到业务系统的 bean/服务 +- TaskAssigneeDispatcher:如何从 BPMN/变量/业务规则生成 assignee 记录 + +--- + +## 1. InstanceAccessor:引擎如何拿你的业务服务 + +典型实现: + +- 在 Spring 环境中:通过 ApplicationContext.getBean(name) +- 在非 Spring 环境中:通过自建容器或反射 newInstance + +你需要保证: + +- delegation/listener/className 能映射到可用实例 +- bean 生命周期可控(单例/原型) + +--- + +## 2. TaskAssigneeDispatcher:分派的唯一入口(强烈建议) + +DataBase 模式下,userTask 的分派信息最终会落到: + +- `se_task_assignee_instance` + +Dispatcher 的职责: + +- 解析 BPMN 声明的候选规则(user/group/org/position) +- 结合流程变量与业务规则,生成最终 assignee 列表 +- 支持动态规则:例如按门店/区域/角色/值班表分派 + +仓库默认实现:`DefaultTaskAssigneeDispatcher` + +你可以做的增强: + +- 对接你的 IAM:把 role/org/position 映射成 userId 列表 +- 支持“排班/轮转”:把候选人计算交给你的排班服务 +- 支持 SLA/优先级:分派时写入 task 扩展字段 + +--- + +## 3. 待办查询与权限 + +生产里“待办查询”通常不是简单按 userId: + +- 个人:userId +- 组:groupId +- 组织:orgId +- 岗位:positionId +- 角色:roleId +- 组合:多条件 + 优先级/标签/时间 + +建议: + +- 引擎表负责“写入最小真相” +- 复杂查询在业务系统侧做 read model(例如任务视图表),避免对引擎表做复杂 join + +--- + +## 4. 任务操作语义(建议一致化) + +在你的系统侧建议统一定义操作: + +- claim(认领) +- unclaim(释放) +- complete(完成) +- transfer(转派) +- addAssignee/removeAssignee(会签/加签/减签) +- rollback(回退) + +并与增强表对齐(转派/回退/操作记录)。 + diff --git a/docs/06-ops/data-retention.md b/docs/06-ops/data-retention.md new file mode 100644 index 000000000..f7ea438de --- /dev/null +++ b/docs/06-ops/data-retention.md @@ -0,0 +1,41 @@ +# 历史数据与归档清理(无历史表时怎么做) + +SmartEngine 默认表结构并没有一套完整的 history 表(虽然存在 `ExecutionHistoryInstanceStorage` 接口)。 + +你在生产里通常需要: + +- 审计(谁在何时做了什么) +- 回放(某实例在某时刻处于什么状态) +- 保留策略(监管/合规/运营需求) +- 清理归档(控制主表体积) + +--- + +## 1. 三种常见策略 + +### 1) 状态表 + 定期清理(最简单) + +- 主表保存 active/closed +- 超过保留期删除 closed 数据 +- 适合:只关心近期数据、审计要求低 + +### 2) 主表 + 归档表(中等) + +- 定期把 closed 数据搬到归档库/归档表 +- 查询历史时走归档库 +- 适合:需要历史查询,但性能/成本可控 + +### 3) 事件日志(最佳) + +- 每次推进都输出事件(append-only) +- 通过事件可重建任意时刻状态(可回放) +- 适合:高审计需求、复杂系统、需要重跑/追溯 + +--- + +## 2. 建议你最少做的事情 + +- 建一张“操作日志表”:记录 start/signal/complete/jump/markDone 等操作 +- 对 userTask 的转派/回退使用增强表并补齐审计字段 +- 建立定时清理任务(按 tenant 维度分批) + diff --git a/docs/06-ops/failure-handling.md b/docs/06-ops/failure-handling.md new file mode 100644 index 000000000..884c21a3f --- /dev/null +++ b/docs/06-ops/failure-handling.md @@ -0,0 +1,52 @@ +# 失败语义(异常如何处理)、重试/补偿/告警 + +生产系统里你最关心的问题是:**某个节点执行失败后,引擎会怎样?我该如何恢复?** + +SmartEngine 的关键组件: + +- `ExceptionProcessor`:异常统一处理入口(默认 `DefaultExceptionProcessor`) +- `@Retryable`:声明重试策略(见 `core/.../annotation/Retryable`) +- retry 扩展模块:`extension/retry/*`(含 DB 存储) + +--- + +## 1. 异常分类建议(推荐) + +- **可重试**:网络超时、外部系统 5xx、资源不足 +- **不可重试**:参数校验失败、权限失败、业务规则拒绝 +- **需补偿**:已产生副作用(扣款/发货),需要反向补偿 + +--- + +## 2. 重试策略 + +推荐策略: + +- 限次(maxAttempts) +- 退避(fixed delay / exponential backoff) +- 可观测(记录每次重试原因与次数) +- 可终止(达到上限后进入人工处理/补偿流程) + +`@Retryable` 用法请参考源码与 retry 模块测试。 + +--- + +## 3. 补偿与人工干预 + +你通常需要三种“人工按钮”: + +- suspend:暂停实例(停止推进) +- jump:跳过故障节点或跳到修复节点 +- markDone:强制标记完成某分支 + +这些权限在 `ExecutionCommandService` 中提供(见 `03-usage/jump-and-advanced.md`)。 + +--- + +## 4. 告警建议 + +- 失败次数阈值告警(同一流程/同一节点) +- 重试耗尽告警 +- 并行 join 长时间等待告警(可能卡死) +- task 超时/SLA 告警 + diff --git a/docs/06-ops/monitoring.md b/docs/06-ops/monitoring.md new file mode 100644 index 000000000..d30d8e62b --- /dev/null +++ b/docs/06-ops/monitoring.md @@ -0,0 +1,51 @@ +# 日志、指标、链路追踪建议 + +SmartEngine 本身是一个嵌入式组件,监控权限需要你在宿主系统里补齐。 + +--- + +## 1. 日志(Logging) + +建议: + +- 为每次 start/signal/complete 生成 traceId(贯穿业务日志) +- 在 delegation/listener 执行时记录: + - processInstanceId / executionInstanceId / activityId + - 业务幂等 key + - 耗时与异常堆栈 + +如果你使用 MDC(SLF4J),建议把关键 id 写入 MDC。 + +--- + +## 2. 指标(Metrics) + +建议你为以下权限暴露指标: + +- start/signal/complete TPS +- 执行耗时分布(histogram) +- 并行网关分支数/等待时间 +- 重试次数、重试耗尽次数 +- task backlog(待办量) + +--- + +## 3. 链路追踪(Tracing) + +在 ServiceTask 的委派调用边界是天然 span: + +- span.name = processId + activityId +- attributes:processInstanceId, executionInstanceId, tenantId, bizKey + +可用 OpenTelemetry / SkyWalking / Zipkin 等实现。 + +--- + +## 4. 数据库观测 + +DataBase 模式建议: + +- 打开慢 SQL 日志 +- 为关键 SQL 做 explain analyze +- 关注锁等待(并行 join 时更明显) + diff --git a/docs/06-ops/performance.md b/docs/06-ops/performance.md new file mode 100644 index 000000000..eb3e7e502 --- /dev/null +++ b/docs/06-ops/performance.md @@ -0,0 +1,51 @@ +# 性能模型、压测建议、瓶颈定位 + +SmartEngine 的性能瓶颈主要来自三部分: + +1) 引擎推进频率(节点数量、并行分支数、signal 次数) +2) 存储写入与查询(DataBase 模式:MyBatis + DB) +3) 外部业务调用(ServiceTask 委派调用、外部系统交互) + +--- + +## 1. 影响性能的关键因子 + +- 并行网关分支数:execution 数量成倍增加 +- userTask 数量:task/assignee 写入量增加 +- 变量大小:variable 表写入与序列化成本增加 +- 查询模式:是否做复杂 join、是否分页、是否按 tenantId 索引 + +--- + +## 2. 压测建议(最小可行) + +### 2.1 指标 + +- start TPS、signal TPS、complete TPS +- 平均推进耗时 / P95 / P99 +- DB:QPS、慢查询、锁等待 +- 线程池:队列积压、拒绝数 + +### 2.2 场景 + +- 顺序流程(基线) +- 并行分叉 + join(并发正确性 + 性能) +- ReceiveTask + 外部 signal(事件驱动) +- userTask 大量待办(查询与分页) + +--- + +## 3. DataBase 模式的优化点 + +- 索引:按 tenantId + status + gmt_modified 组合索引 +- 批量写入:尽量减少频繁 update +- 查询下沉:复杂待办查询做 read model + +--- + +## 4. Custom 模式的优化点 + +- 存储接口实现:避免“每推进一步都落库多表” +- 事件日志:用 append-only 模式减少写放大 +- 幂等:避免重复推进导致的额外调用 + diff --git a/docs/07-dev/architecture.md b/docs/07-dev/architecture.md new file mode 100644 index 000000000..538922d9a --- /dev/null +++ b/docs/07-dev/architecture.md @@ -0,0 +1,89 @@ +# 架构:core / extension / docs 的模块划分与依赖关系 + +SmartEngine 仓库按 Maven 多模块组织(根 `pom.xml`): + +```text +- core +- extension/storage/storage-common +- extension/storage/storage-custom +- extension/storage/storage-mysql +- extension/retry/retry-common +- extension/retry/retry-custom +- extension/retry/retry-mysql +``` + + +--- + +## 1. core(引擎内核) + +目录:`core/` + +职责: + +- BPMN 解析(parser) +- 执行模型(process/execution/activity/token) +- 行为实现(serviceTask/receiveTask/gateway…) +- 对外服务接口(SmartEngine + Command/QueryService) +- 扩展机制(ExtensionBinding、AnnotationScanner、ExtensionContainer) +- 常量、工具、异常处理(ExceptionProcessor 等) + +依赖要求尽量轻(README 中强调 “Less Dependent / avoid JAR hell”)。 + +--- + +## 2. extension(可插拔扩展) + +目录:`extension/` + +### 2.1 storage(存储实现) + +- `storage-custom`:测试/示例存根,实现 Storage 接口但不落库 +- `storage-mysql`:DataBase 模式实现(MyBatis + DDL + SQLMap) + +### 2.2 retry(失败重试) + +- `retry-common`:注解与公共抽象(@Retryable 等) +- `retry-mysql`:重试记录的关系库存储(含 DDL) + +--- + +## 3. ecology(生态/工具) + +目录:`ecology/` + +仓库中包含一些与“建模/设计器/适配”相关的代码(例如 designer),通常不影响 core 的运行期语义,但对平台化落地有价值(例如你做低代码建模器时可以复用其中的结构)。 + +--- + +## 4. 依赖方向(建议) + +```mermaid +flowchart TB + core --> extension_storage_custom + core --> extension_storage_mysql + core --> extension_retry_common + extension_retry_mysql --> extension_retry_common + extension_storage_mysql --> core + extension_storage_custom --> core +``` + +原则: + +- core 不依赖具体存储 +- extension 依赖 core +- 不要在 core 中引入强耦合框架(保持可嵌入性) + +--- + +## 5. 你做二开时的推荐分层 + +如果你要做“SmartEngine + 业务平台”的深度集成,建议你在业务仓库中按以下分层组织: + +- `engine-adapter`:对 SmartEngine 的封装(tenant、幂等、审计、监控) +- `engine-storage-impl`:自研 Storage(支持多 DB/历史/归档) +- `engine-user-integration`:组织/角色/权限/待办查询 read model +- `engine-ops`:监控、告警、数据清理、压测脚本 + +这样你可以保持引擎内核的可升级性,同时把“平台特有权限”外置。 + diff --git a/docs/07-dev/build-and-test.md b/docs/07-dev/build-and-test.md new file mode 100644 index 000000000..01a76518b --- /dev/null +++ b/docs/07-dev/build-and-test.md @@ -0,0 +1,61 @@ +# 构建、单测、集成测试、测试数据/用例结构 + +--- + +## 1. 构建 + +在仓库根目录执行: + +```bash +mvn -q -DskipTests=false test +``` + +按模块构建: + +```bash +mvn -pl core -am test +mvn -pl extension/storage/storage-custom -am test +mvn -pl extension/storage/storage-mysql -am test +``` + +> 提示:DataBase 模式测试通常需要本地数据库(默认配置在 `storage-mysql/src/test/resources/spring/application-test.xml`)。 + +--- + +## 2. 单元测试 vs 集成测试 + +- core:以 parser/behavior 的单元测试为主(不依赖数据库) +- storage-custom:以“语义回归测试”为主(并行网关、跳转等) +- storage-mysql:以“存储 + MyBatis + 事务”的集成测试为主(依赖数据库) + +--- + +## 3. 测试资源(BPMN) + +常见目录: + +- `core/src/test/resources/process-def/*` +- `extension/storage/storage-custom/src/test/resources/*.xml` +- `extension/storage/storage-mysql/src/test/resources/*.bpmn20.xml` + +建议你把这些 BPMN 当作“语义金标准”,二开时新增行为/解析器要新增对应 BPMN + 测试。 + +--- + +## 4. 测试数据库准备(DataBase 模式) + +仓库未提供 docker-compose,但你可以用本地 PostgreSQL / MySQL: + +1) 创建数据库与用户 +2) 执行 DDL(见 `04-persistence/database-schema.md`) +3) 修改 `application-test.xml` 的 JDBC URL/账号密码 +4) 跑 `storage-mysql` 模块测试 + +--- + +## 5. CI 建议(如果你要在团队里用) + +- MySQL + PostgreSQL 双跑(避免方言回归) +- 并行网关相关测试要单独作为 “语义回归” job +- 对关键 SQL 做 explain 基线(防止性能回归) + diff --git a/docs/07-dev/contributing.md b/docs/07-dev/contributing.md new file mode 100644 index 000000000..95ff451ff --- /dev/null +++ b/docs/07-dev/contributing.md @@ -0,0 +1,47 @@ +# 贡献指南(Contributing) + +本指南面向团队内二开/贡献场景,建议与你仓库根目录的 `CONTRIBUTING` 保持一致(若存在)。 + +--- + +## 1. 代码风格与原则 + +- 保持 core 依赖轻:不要在 core 引入重型框架 +- 新增权限优先通过扩展点实现,而不是改动核心语义 +- 每个新特性必须配套: + - 单元测试 / 集成测试 + - 示例 BPMN(如涉及解析/行为) + - 文档(建议同步更新本 docs 目录) + +--- + +## 2. PR 要求(建议) + +- PR 描述必须包含: + - 背景/问题 + - 方案 + - 风险与兼容性 + - 测试用例与结果 +- 如果涉及数据库变更: + - 同步更新 MySQL 与 PostgreSQL DDL + - 同步更新 SQLMap + - 增加迁移说明 + +--- + +## 3. 回归测试清单(推荐) + +- core:parser/behavior 全量测试 +- storage-custom:并行网关 + 跳转测试 +- storage-mysql:流程启动 + userTask + 变量 + 并行网关(至少一套) + +--- + +## 4. 文档同步原则 + +- 任何对外行为变更,必须同步更新: + - `02-concepts/`(概念/语义) + - `03-usage/`(用法) + - `04-persistence/`(表结构/SQL) + - `05-extensibility/`(扩展点) + diff --git a/docs/07-dev/release.md b/docs/07-dev/release.md new file mode 100644 index 000000000..f8fa39384 --- /dev/null +++ b/docs/07-dev/release.md @@ -0,0 +1,43 @@ +# 版本号、变更日志、兼容性与发布流程(建议) + +SmartEngine 仓库未强制规定发布流程,但如果你要在企业内“可控升级”,建议建立以下规范。 + +--- + +## 1. 版本号建议(SemVer) + +- MAJOR:破坏兼容(API/语义/表结构) +- MINOR:向后兼容新增权限 +- PATCH:向后兼容修复 + +--- + +## 2. 变更日志(Changelog) + +每次发布至少记录: + +- 新增:新增扩展点、新增行为、新增表/字段 +- 修复:修复的 bug、影响范围 +- 兼容性:是否需要重跑所有流程定义、是否需要数据迁移 + +--- + +## 3. 数据库迁移策略(DataBase 模式) + +- 所有 DDL 变更要同时提供: + - MySQL + - PostgreSQL +- 建议引入 Flyway/Liquibase 管理迁移脚本(业务系统侧做) +- 避免直接修改存量表类型(会导致迁移成本与风险飙升) + +--- + +## 4. 发布流程(企业内建议) + +1) 在 feature 分支完成开发 + 测试 +2) 合并到 dev,跑全量 CI(MySQL + PostgreSQL) +3) 打 RC(候选版),在预发环境回归 +4) 发布正式版(tag + changelog) +5) 灰度发布(按租户/按业务线) +6) 监控与回滚预案(表结构变更要提前演练) + diff --git a/docs/ProcessMonitorSolution.md b/docs/ProcessMonitorSolution.md new file mode 100644 index 000000000..fc3b94741 --- /dev/null +++ b/docs/ProcessMonitorSolution.md @@ -0,0 +1,956 @@ +# SmartEngine 流程监控日志方案 + +## 概述 + +本文档提供基于SmartEngine框架的流程监控日志实现方案,通过组合使用以下两种机制实现: + +1. **setter注入** - 通过`ProcessEngineConfiguration`的setter方法注入监控组件 +2. **@ExtensionBinding机制** - 对于无法通过setter注入的组件(如TransitionBehavior) + +**注意**: 不是所有组件都能通过setter注入,需要根据组件类型选择合适的方式。 + +--- + +## 一、监控需求分析 + +### 1.1 核心监控点 + +| 监控点 | 说明 | 关键类 | +|--------|------|--------| +| 流程启动/终止 | 流程实例创建与销毁 | `ProcessCommandService.start()/abort()` | +| 节点进入 | 活动节点开始执行 | `AbstractActivityBehavior.enter()` | +| 节点执行 | 节点业务逻辑处理 | `AbstractActivityBehavior.execute()` | +| 节点离开 | 节点执行完毕流向下一节点 | `AbstractActivityBehavior.leave()` | +| 网关决策 | 条件判断与路径选择 | `CommonGatewayHelper.calcMatchedTransitions()` | +| 条件表达式 | 表达式计算结果 | `ExpressionUtil.eval()` | +| 异常捕获 | 错误信息记录 | `ExceptionProcessor` | +| 变量变更 | 流程变量读写 | `VariableInstanceStorage` | + +### 1.2 监控数据结构 + +```java +@Data +public class ProcessMonitorEvent { + private String eventType; // START/ENTER/EXECUTE/LEAVE/GATEWAY/ERROR + private String processInstanceId; // 流程实例ID + private String processDefinitionId; // 流程定义ID + private String activityId; // 当前活动节点ID + private String activityType; // 活动节点类型 + private String transitionId; // 网关决策时的流转ID + private String conditionExpression; // 条件表达式内容 + private Boolean conditionResult; // 条件判断结果 + private Map inputVariables; // 输入变量 + private Map outputVariables; // 输出变量 + private long duration; // 执行耗时(毫秒) + private String errorMessage; // 错误信息 + private LocalDateTime timestamp; // 时间戳 +} +``` + +--- + +## 二、监控组件实现 + +### 2.1 MonitoringDelegationExecutor - 委托执行监控 + +**作用范围**: 所有ServiceTask、ScriptTask、BusinessRuleTask等委托行为 + +```java +package com.example.smartengine.monitor; + +import com.alibaba.smart.framework.engine.configuration.DelegationExecutor; +import com.alibaba.smart.framework.engine.context.ExecutionContext; +import com.alibaba.smart.framework.engine.model.assembly.Activity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MonitoringDelegationExecutor implements DelegationExecutor { + + private static final Logger MONITOR_LOG = LoggerFactory.getLogger("ProcessMonitor"); + private static final Logger ERROR_LOG = LoggerFactory.getLogger("ProcessError"); + + private final DelegationExecutor delegate; + + public MonitoringDelegationExecutor(DelegationExecutor delegate) { + this.delegate = delegate; + } + + @Override + public void execute(ExecutionContext context, Activity activity) { + String processInstanceId = context.getProcessInstance().getInstanceId(); + String activityId = activity.getId(); + String activityType = activity.getClass().getSimpleName(); + + MONITOR_LOG.info("[MONITOR] ActivityStart processInstanceId={} activityId={} activityType={}", + processInstanceId, activityId, activityType); + + long startTime = System.currentTimeMillis(); + try { + delegate.execute(context, activity); + long duration = System.currentTimeMillis() - startTime; + + MONITOR_LOG.info("[MONITOR] ActivityComplete processInstanceId={} activityId={} duration={}ms", + processInstanceId, activityId, duration); + + } catch (Exception e) { + long duration = System.currentTimeMillis() - startTime; + ERROR_LOG.error("[MONITOR] ActivityError processInstanceId={} activityId={} duration={}ms error={}", + processInstanceId, activityId, duration, e.getMessage(), e); + throw e; + } + } +} +``` + +### 2.2 MonitoringListenerExecutor - 监听器执行监控 + +**作用范围**: 执行监听器(start、end、take事件) + +```java +package com.example.smartengine.monitor; + +import com.alibaba.smart.framework.engine.configuration.ListenerExecutor; +import com.alibaba.smart.framework.engine.context.ExecutionContext; +import com.alibaba.smart.framework.engine.model.assembly.ExtensionElementContainer; +import com.alibaba.smart.framework.engine.pvm.event.EventConstant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MonitoringListenerExecutor implements ListenerExecutor { + + private static final Logger MONITOR_LOG = LoggerFactory.getLogger("ListenerMonitor"); + + private final ListenerExecutor delegate; + + public MonitoringListenerExecutor(ListenerExecutor delegate) { + this.delegate = delegate; + } + + @Override + public void execute(EventConstant event, ExtensionElementContainer container, ExecutionContext context) { + String eventName = event.name(); + String processInstanceId = context.getProcessInstance().getInstanceId(); + String activityId = container.getId(); + + MONITOR_LOG.info("[LISTENER] EventStart event={} processInstanceId={} activityId={}", + eventName, processInstanceId, activityId); + + long startTime = System.currentTimeMillis(); + try { + delegate.execute(event, container, context); + long duration = System.currentTimeMillis() - startTime; + + MONITOR_LOG.info("[LISTENER] EventComplete event={} processInstanceId={} activityId={} duration={}ms", + eventName, processInstanceId, activityId, duration); + + } catch (Exception e) { + long duration = System.currentTimeMillis() - startTime; + MONITOR_LOG.error("[LISTENER] EventError event={} processInstanceId={} activityId={} error={}", + eventName, processInstanceId, activityId, e.getMessage()); + throw e; + } + } +} +``` + +### 2.3 MonitoringExpressionEvaluator - 表达式计算监控 + +**作用范围**: 所有表达式计算(条件表达式、任务完成条件等) + +**监控原理**: `ExpressionUtil`是静态工具类,内部调用`ExpressionEvaluator`,因此需要包装`ExpressionEvaluator`才能监控表达式计算 + +```java +package com.example.smartengine.monitor; + +import com.alibaba.smart.framework.engine.common.expression.evaluator.ExpressionEvaluator; +import com.alibaba.smart.framework.engine.configuration.ConfigurationOption; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; + +public class MonitoringExpressionEvaluator implements ExpressionEvaluator { + + private static final Logger EXPR_LOG = LoggerFactory.getLogger("ExpressionMonitor"); + + private final ExpressionEvaluator delegate; + + public MonitoringExpressionEvaluator(ExpressionEvaluator delegate) { + this.delegate = delegate; + } + + @Override + public Object eval(String expression, Map requestContext, boolean cacheEnabled) { + EXPR_LOG.debug("[EXPR] Evaluating expression={} cacheEnabled={}", expression, cacheEnabled); + + Object result = delegate.eval(expression, requestContext, cacheEnabled); + + EXPR_LOG.info("[EXPR] ExpressionResult expression={} result={}", expression, result); + + return result; + } +} +``` + +**配置启用**: +```java +config.setExpressionEvaluator( + new MonitoringExpressionEvaluator(config.getExpressionEvaluator()) +); +``` + +### 2.4 MonitoringExceptionProcessor - 异常处理监控 + +**作用范围**: 所有流程执行异常 + +```java +package com.example.smartengine.monitor; + +import com.alibaba.smart.framework.engine.configuration.ExceptionProcessor; +import com.alibaba.smart.framework.engine.context.ExecutionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MonitoringExceptionProcessor implements ExceptionProcessor { + + private static final Logger ERROR_LOG = LoggerFactory.getLogger("ProcessError"); + + @Override + public void process(Exception exception, ExecutionContext context) { + String processInstanceId = "N/A"; + String activityId = "N/A"; + + if (context != null && context.getProcessInstance() != null) { + processInstanceId = context.getProcessInstance().getInstanceId(); + } + + if (context != null && context.getExecutionInstance() != null) { + activityId = context.getExecutionInstance().getProcessDefinitionActivityId(); + } + + ERROR_LOG.error("[ERROR] ProcessError processInstanceId={} activityId={} error={}", + processInstanceId, activityId, exception.getMessage(), exception); + } +} +``` + +### 2.5 MonitoringVariablePersister - 变量变更监控 + +**作用范围**: 流程变量读写操作 + +```java +package com.example.smartengine.monitor; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.model.instance.VariableInstance; +import com.alibaba.smart.framework.engine.persister.variable.VariablePersister; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class MonitoringVariablePersister implements VariablePersister { + + private static final Logger VAR_LOG = LoggerFactory.getLogger("VariableMonitor"); + + private final VariablePersister delegate; + + public MonitoringVariablePersister(VariablePersister delegate) { + this.delegate = delegate; + } + + @Override + public void save(VariableInstance variableInstance, ProcessEngineConfiguration config) { + String processInstanceId = variableInstance.getProcessInstanceId(); + String variableName = variableInstance.getName(); + Object variableValue = variableInstance.getValue(); + + VAR_LOG.info("[VAR] Save processInstanceId={} name={} value={}", + processInstanceId, variableName, variableValue); + + delegate.save(variableInstance, config); + } + + @Override + public void save(List variableInstances, ProcessEngineConfiguration config) { + for (VariableInstance variableInstance : variableInstances) { + save(variableInstance, config); + } + delegate.save(variableInstances, config); + } + + @Override + public Map findByProcessInstanceId(String processInstanceId, ProcessEngineConfiguration config) { + VAR_LOG.debug("[VAR] FindByProcessInstanceId processInstanceId={}", processInstanceId); + return delegate.findByProcessInstanceId(processInstanceId, config); + } + + @Override + public void update(VariableInstance variableInstance, ProcessEngineConfiguration config) { + VAR_LOG.info("[VAR] Update processInstanceId={} name={} newValue={}", + variableInstance.getProcessInstanceId(), variableInstance.getName(), variableInstance.getValue()); + delegate.update(variableInstance, config); + } + + @Override + public void remove(Set names, ProcessEngineConfiguration config) { + VAR_LOG.info("[VAR] Remove names={}", names); + delegate.remove(names, config); + } +} +``` + +### 2.6 MonitoringSequenceFlowBehavior - 网关决策监控 + +**作用范围**: ExclusiveGateway、InclusiveGateway的条件判断路径选择 + +**监控原理**: `CommonGatewayHelper`是静态工具类,无法通过setter注入。需要通过`@ExtensionBinding`机制替换默认的`TransitionBehavior` + +```java +package com.example.smartengine.monitor; + +import com.alibaba.smart.framework.engine.behavior.TransitionBehavior; +import com.alibaba.smart.framework.engine.behavior.base.AbstractTransitionBehavior; +import com.alibaba.smart.framework.engine.bpmn.assembly.process.SequenceFlow; +import com.alibaba.smart.framework.engine.common.expression.ExpressionUtil; +import com.alibaba.smart.framework.engine.context.ExecutionContext; +import com.alibaba.smart.framework.engine.extension.annotation.ExtensionBinding; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.model.assembly.ConditionExpression; +import com.alibaba.smart.framework.engine.model.assembly.Transition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ExtensionBinding(group = ExtensionConstant.ACTIVITY_BEHAVIOR, bindKey = TransitionBehavior.class) +public class MonitoringSequenceFlowBehavior extends AbstractTransitionBehavior { + + private static final Logger GATEWAY_LOG = LoggerFactory.getLogger("GatewayMonitor"); + + @Override + public boolean match(ExecutionContext context, Transition transition) { + ConditionExpression conditionExpression = transition.getConditionExpression(); + String processInstanceId = context.getProcessInstance().getInstanceId(); + String transitionId = transition.getId(); + + if (conditionExpression != null) { + String expressionContent = conditionExpression.getExpressionContent(); + Boolean result = ExpressionUtil.eval(context, conditionExpression); + + GATEWAY_LOG.info("[GATEWAY] ConditionCheck processInstanceId={} transitionId={} expression={} result={}", + processInstanceId, transitionId, expressionContent, result); + + return result; + } + + GATEWAY_LOG.info("[GATEWAY] NoCondition processInstanceId={} transitionId={}", + processInstanceId, transitionId); + + return true; + } +} +``` + +**注意**: 此组件使用`@ExtensionBinding`机制,不能通过setter注入,需要确保包扫描能扫描到此注解类。 + +--- + +## 三、配置启用 + +### 3.1 方式一:编程式配置(推荐) + +```java +package com.example.smartengine.config; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultProcessEngineConfiguration; +import com.example.smartengine.monitor.*; + +public class MonitorEnabledProcessEngineConfiguration { + + public static ProcessEngineConfiguration create() { + // 创建默认配置 + DefaultProcessEngineConfiguration config = new DefaultProcessEngineConfiguration(); + + // 包装 DelegationExecutor - 监控节点执行 + config.setDelegationExecutor( + new MonitoringDelegationExecutor(config.getDelegationExecutor()) + ); + + // 包装 ListenerExecutor - 监控生命周期事件 + config.setListenerExecutor( + new MonitoringListenerExecutor(config.getListenerExecutor()) + ); + + // 设置 ExceptionProcessor - 监控异常信息 + config.setExceptionProcessor(new MonitoringExceptionProcessor()); + + // 包装 VariablePersister - 监控变量变更 + config.setVariablePersister( + new MonitoringVariablePersister(config.getVariablePersister()) + ); + + return config; + } +} +``` + +### 3.2 方式二:Spring Boot 配置类 + +```java +package com.example.smartengine.config; + +import com.alibaba.smart.framework.engine.configuration.DelegationExecutor; +import com.alibaba.smart.framework.engine.configuration.ListenerExecutor; +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultProcessEngineConfiguration; +import com.example.smartengine.monitor.*; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +@Configuration +public class SmartEngineMonitorConfiguration { + + @Bean + @Primary + public ProcessEngineConfiguration processEngineConfiguration() { + DefaultProcessEngineConfiguration config = new DefaultProcessEngineConfiguration(); + + // 包装 DelegationExecutor + config.setDelegationExecutor( + new MonitoringDelegationExecutor(config.getDelegationExecutor()) + ); + + // 包装 ListenerExecutor + config.setListenerExecutor( + new MonitoringListenerExecutor(config.getListenerExecutor()) + ); + + // 设置 ExceptionProcessor + config.setExceptionProcessor(new MonitoringExceptionProcessor()); + + // 包装 VariablePersister + config.setVariablePersister( + new MonitoringVariablePersister(config.getVariablePersister()) + ); + + return config; + } +} +``` + +### 3.3 方式三:测试基类中配置 + +```java +package com.example.smartengine.test; + +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultProcessEngineConfiguration; +import com.example.smartengine.monitor.*; +import org.junit.Before; + +public abstract class MonitorEnabledTestCase { + + protected ProcessEngineConfiguration processEngineConfiguration; + + @Before + public void initMonitorConfiguration() { + DefaultProcessEngineConfiguration config = new DefaultProcessEngineConfiguration(); + + // 包装 DelegationExecutor + config.setDelegationExecutor( + new MonitoringDelegationExecutor(config.getDelegationExecutor()) + ); + + // 包装 ListenerExecutor + config.setListenerExecutor( + new MonitoringListenerExecutor(config.getListenerExecutor()) + ); + + // 设置 ExceptionProcessor + config.setExceptionProcessor(new MonitoringExceptionProcessor()); + + // 包装 VariablePersister + config.setVariablePersister( + new MonitoringVariablePersister(config.getVariablePersister()) + ); + + this.processEngineConfiguration = config; + } +} +``` + +### 3.4 方式四:扩展DefaultSmartEngine + +```java +package com.example.smartengine.config; + +import com.alibaba.smart.framework.engine.SmartEngine; +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultSmartEngine; +import com.example.smartengine.monitor.*; + +public class MonitorEnabledSmartEngine extends DefaultSmartEngine { + + @Override + protected ProcessEngineConfiguration createProcessEngineConfiguration() { + ProcessEngineConfiguration config = super.createProcessEngineConfiguration(); + + // 包装 DelegationExecutor + config.setDelegationExecutor( + new MonitoringDelegationExecutor(config.getDelegationExecutor()) + ); + + // 包装 ListenerExecutor + config.setListenerExecutor( + new MonitoringListenerExecutor(config.getListenerExecutor()) + ); + + // 设置 ExceptionProcessor + config.setExceptionProcessor(new MonitoringExceptionProcessor()); + + // 包装 VariablePersister + config.setVariablePersister( + new MonitoringVariablePersister(config.getVariablePersister()) + ); + + return config; + } +} +``` + +--- + +## 四、扩展机制说明 + +### 4.1 两种扩展机制对比 + +| 机制 | 适用场景 | 优点 | 缺点 | +|------|---------|------|------| +| **setter注入** | 实现了接口的组件(DelegationExecutor、ListenerExecutor等) | 简单直观、可控性强 | 仅适用于有接口的组件 | +| **@ExtensionBinding** | ActivityBehavior、TransitionBehavior等行为类 | 无需setter、自动发现 | 需要包扫描配合、覆盖逻辑较复杂 | + +### 4.2 组件扩展方式选择指南 + +**可以通过setter注入的组件**: +- `DelegationExecutor` - 委托执行器 +- `ListenerExecutor` - 监听器执行器 +- `ExceptionProcessor` - 异常处理器 +- `VariablePersister` - 变量持久化 +- `ExpressionEvaluator` - 表达式计算器 +- 其他在`ProcessEngineConfiguration`中定义了setter的组件 + +**必须通过@ExtensionBinding的组件**: +- `TransitionBehavior` - 流转行为(监控网关决策) +- `ActivityBehavior`实现类 - 活动行为(监控特定节点类型) +- `ElementParser`实现类 - 元素解析器 +- `Service`实现类 - 服务实现 + +### 4.3 静态工具类的处理 + +以下静态工具类无法直接注入,需要间接监控: + +| 工具类 | 间接监控方式 | +|--------|-------------| +| `ExpressionUtil` | 包装`ExpressionEvaluator`(`ExpressionUtil`内部调用它) | +| `CommonGatewayHelper` | 通过`TransitionBehavior`的`@ExtensionBinding`机制 | + +### 4.4 混合使用示例 + +```java +public static ProcessEngineConfiguration create() { + DefaultProcessEngineConfiguration config = new DefaultProcessEngineConfiguration(); + + // 1. setter注入方式 + config.setDelegationExecutor( + new MonitoringDelegationExecutor(config.getDelegationExecutor()) + ); + config.setExpressionEvaluator( + new MonitoringExpressionEvaluator(config.getExpressionEvaluator()) + ); + config.setExceptionProcessor(new MonitoringExceptionProcessor()); + + // 2. @ExtensionBinding方式(通过包扫描自动发现) + // 无需手动配置,框架会自动扫描@ExtensionBinding注解的类 + + return config; +} +``` + +--- + +## 五、可配置组件列表 + +### 4.1 DefaultProcessEngineConfiguration 可用setter + +| setter方法 | 默认实现 | 监控作用 | +|------------|---------|---------| +| `setDelegationExecutor` | `DefaultDelegationExecutor` | 监控节点业务逻辑执行 | +| `setListenerExecutor` | `DefaultListenerExecutor` | 监控生命周期事件 | +| `setExceptionProcessor` | `DefaultExceptionProcessor` | 监控异常信息 | +| `setVariablePersister` | `DefaultVariablePersister` | 监控变量读写 | +| `setExpressionEvaluator` | `MvelExpressionEvaluator` | 表达式计算(可扩展监控) | +| `setIdGenerator` | `DefaultIdGenerator` | ID生成监控 | +| `setInstanceAccessor` | `DefaultInstanceAccessor` | 实例访问监控 | +| `setAnnotationScanner` | `SimpleAnnotationScanner` | 注解扫描监控 | +| `setParallelServiceOrchestration` | `DefaultParallelServiceOrchestration` | 并行编排监控 | +| `setTaskAssigneeDispatcher` | - | 任务分配监控 | +| `setMultiInstanceCounter` | - | 会签计数器监控 | +| `setLockStrategy` | - | 锁策略监控 | +| `setExecutorService` | - | 线程池监控 | +| `setStorageRouter` | `StorageRouter` | 存储路由监控 | +| `setDialect` | - | 方言监控 | + +### 4.2 配置优先级 + +``` +显式setter > 默认配置 +``` + +当通过setter显式设置组件时,会覆盖框架的默认实现。 + +--- + +## 五、性能优化建议 + +### 5.1 异步日志处理 + +```java +package com.example.smartengine.monitor; + +import com.alibaba.smart.framework.engine.configuration.DelegationExecutor; +import com.alibaba.smart.framework.engine.context.ExecutionContext; +import com.alibaba.smart.framework.engine.model.assembly.Activity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class AsyncMonitoringDelegationExecutor implements DelegationExecutor { + + private static final Logger MONITOR_LOG = LoggerFactory.getLogger("ProcessMonitor"); + private static final ExecutorService MONITOR_EXECUTOR = Executors.newFixedThreadPool(4); + + private final DelegationExecutor delegate; + + public AsyncMonitoringDelegationExecutor(DelegationExecutor delegate) { + this.delegate = delegate; + } + + @Override + public void execute(ExecutionContext context, Activity activity) { + String processInstanceId = context.getProcessInstance().getInstanceId(); + String activityId = activity.getId(); + + // 异步记录开始日志 + MONITOR_EXECUTOR.submit(() -> + MONITOR_LOG.info("[MONITOR] ActivityStart processInstanceId={} activityId={}", + processInstanceId, activityId) + ); + + long startTime = System.currentTimeMillis(); + try { + delegate.execute(context, activity); + long duration = System.currentTimeMillis() - startTime; + + // 异步记录完成日志 + MONITOR_EXECUTOR.submit(() -> + MONITOR_LOG.info("[MONITOR] ActivityComplete processInstanceId={} activityId={} duration={}ms", + processInstanceId, activityId, duration) + ); + + } catch (Exception e) { + long duration = System.currentTimeMillis() - startTime; + MONITOR_LOG.error("[MONITOR] ActivityError processInstanceId={} activityId={} duration={}ms error={}", + processInstanceId, activityId, duration, e.getMessage(), e); + throw e; + } + } +} +``` + +### 5.2 采样策略 + +```java +package com.example.smartengine.monitor; + +public class SamplingMonitorConfig { + + private static final double DEFAULT_SAMPLE_RATE = 0.1; // 默认10%采样 + + private final double sampleRate; + + public SamplingMonitorConfig(double sampleRate) { + this.sampleRate = sampleRate; + } + + public SamplingMonitorConfig() { + this(DEFAULT_SAMPLE_RATE); + } + + public boolean shouldSample() { + return Math.random() < sampleRate; + } + + public static SamplingMonitorConfig createByRate(double rate) { + return new SamplingMonitorConfig(rate); + } +} +``` + +### 5.3 日志级别控制 + +```yaml +# application.yml +logging: + level: + root: INFO + ProcessMonitor: DEBUG + ListenerMonitor: DEBUG + ExpressionMonitor: DEBUG + VariableMonitor: DEBUG + ProcessError: ERROR +``` + +### 5.4 敏感数据脱敏 + +```java +package com.example.smartengine.monitor; + +import java.util.*; + +public class VariableMasker { + + private static final Set SENSITIVE_KEYS = new HashSet<>(Arrays.asList( + "password", "secret", "token", "creditCard", "cvv" + )); + + public static Map maskSensitiveData(Map variables) { + if (variables == null) { + return null; + } + Map masked = new HashMap<>(variables); + for (String key : SENSITIVE_KEYS) { + if (masked.containsKey(key)) { + masked.put(key, "***MASKED***"); + } + } + return masked; + } + + public static boolean isSensitiveKey(String key) { + return SENSITIVE_KEYS.stream().anyMatch(key::toLowerCase); + } +} +``` + +### 5.5 批量日志发送 + +```java +package com.example.smartengine.monitor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.concurrent.*; + +public class BatchMonitorLog { + + private static final Logger BATCH_LOG = LoggerFactory.getLogger("BatchMonitor"); + private static final int BATCH_SIZE = 100; + private static final long FLUSH_INTERVAL_MS = 5000; + + private final Queue eventQueue = new LinkedBlockingQueue<>(10000); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + public BatchMonitorLog() { + scheduler.scheduleAtFixedRate(this::flush, FLUSH_INTERVAL_MS, FLUSH_INTERVAL_MS, TimeUnit.MILLISECONDS); + } + + public void addEvent(ProcessMonitorEvent event) { + eventQueue.offer(event); + if (eventQueue.size() >= BATCH_SIZE) { + flush(); + } + } + + private synchronized void flush() { + if (eventQueue.isEmpty()) { + return; + } + + List batch = new ArrayList<>(); + eventQueue.drainTo(batch, BATCH_SIZE); + + if (!batch.isEmpty()) { + BATCH_LOG.info("[BATCH_MONITOR] BatchSize={} Events={}", batch.size(), batch); + } + } +} +``` + +--- + +## 六、监控日志示例 + +### 6.1 流程启动日志 + +``` +[MONITOR] ActivityStart processInstanceId=PI_123456 activityId=start_event activityType=StartEventBehavior +[MONITOR] ActivityComplete processInstanceId=PI_123456 activityId=start_event duration=1ms +``` + +### 6.2 节点执行日志 + +``` +[MONITOR] ActivityStart processInstanceId=PI_123456 activityId=validate_order activityType=ServiceTaskBehavior +[MONITOR] ActivityComplete processInstanceId=PI_123456 activityId=validate_order duration=15ms +``` + +### 6.3 网关决策日志 + +``` +[EXPR] Evaluating type=uel expression=order.amount > 1000 variables={order={amount=1500}} +[EXPR] ExpressionResult type=uel expression=order.amount > 1000 result=true +``` + +### 6.4 变量变更日志 + +``` +[VAR] Save processInstanceId=PI_123456 name=orderStatus value=APPROVED +[VAR] Update processInstanceId=PI_123456 name=orderAmount newValue=1500.00 +``` + +### 6.5 异常日志 + +``` +[ERROR] ProcessError processInstanceId=PI_123456 activityId=calculate_discount error=NullPointerException +java.lang.NullPointerException + at com.example.DiscountCalculator.calculate(DiscountCalculator.java:45) + at com.alibaba.smart.framework.engine.delegation.JavaDelegation.execute(JavaDelegation.java:32) + ... +``` + +--- + +## 七、集成建议 + +### 7.1 日志收集 + +推荐使用ELK(Elasticsearch + Logstash + Kibana)或EFK(Elasticsearch + Fluentd + Kibana)收集和查询监控日志。 + +### 7.2 链路追踪 + +可集成SkyWalking或Zipkin实现分布式链路追踪: + +```java +// 添加TraceID +String traceId = Tracer.currentSpan().getTraceId(); +MONITOR_LOG.info("[MONITOR] ActivityStart traceId={} processInstanceId={} ...", + traceId, processInstanceId); +``` + +### 7.3 告警规则 + +建议配置以下告警规则: + +| 规则 | 阈值 | 级别 | +|------|------|------| +| 流程执行超时 | 单个节点 > 30s | WARNING | +| 流程执行超时 | 单个节点 > 60s | CRITICAL | +| 节点执行失败 | 连续3次 | WARNING | +| 异常重复出现 | 同一流程实例 > 5次 | CRITICAL | +| 网关决策异常 | 无匹配路径 | CRITICAL | + +--- + +## 八、完整配置示例 + +```java +package com.example.smartengine.config; + +import com.alibaba.smart.framework.engine.configuration.DelegationExecutor; +import com.alibaba.smart.framework.engine.configuration.ExceptionProcessor; +import com.alibaba.smart.framework.engine.configuration.ListenerExecutor; +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.persister.variable.VariablePersister; +import com.example.smartengine.monitor.*; + +public class CompleteMonitorConfiguration { + + public static ProcessEngineConfiguration create() { + DefaultProcessEngineConfiguration config = new DefaultProcessEngineConfiguration(); + + // 1. 包装 DelegationExecutor - 监控所有委托行为 + config.setDelegationExecutor( + new MonitoringDelegationExecutor(config.getDelegationExecutor()) + ); + + // 2. 包装 ListenerExecutor - 监控生命周期事件 + config.setListenerExecutor( + new MonitoringListenerExecutor(config.getListenerExecutor()) + ); + + // 3. 设置 ExceptionProcessor - 监控异常 + config.setExceptionProcessor(new MonitoringExceptionProcessor()); + + // 4. 包装 VariablePersister - 监控变量 + config.setVariablePersister( + new MonitoringVariablePersister(config.getVariablePersister()) + ); + + // 5. 可选:设置自定义ID生成器 + // config.setIdGenerator(new CustomIdGenerator()); + + // 6. 可选:设置自定义表达式计算器(带监控) + // config.setExpressionEvaluator(new MonitoringExpressionEvaluator(config.getExpressionEvaluator())); + + return config; + } +} +``` + +--- + +## 九、总结 + +本方案组合使用`setter注入`和`@ExtensionBinding`两种机制,实现完整的流程监控。 + +### 监控组件与扩展机制对应关系 + +| 监控目标 | 扩展机制 | 配置方式 | +|---------|---------|---------| +| 委托执行(ServiceTask等) | setter注入 | `config.setDelegationExecutor()` | +| 生命周期事件监听 | setter注入 | `config.setListenerExecutor()` | +| 异常处理 | setter注入 | `config.setExceptionProcessor()` | +| 变量读写 | setter注入 | `config.setVariablePersister()` | +| 表达式计算 | setter注入 | `config.setExpressionEvaluator()` | +| 网关条件决策 | @ExtensionBinding | `@ExtensionBinding(group="ACTIVITY_BEHAVIOR", bindKey=TransitionBehavior.class)` | + +### 核心要点 + +1. **setter注入** - 适用于实现了接口的组件(DelegationExecutor、ListenerExecutor等) +2. **@ExtensionBinding** - 适用于ActivityBehavior、TransitionBehavior等行为类 +3. **静态工具类处理** - ExpressionUtil需包装ExpressionEvaluator,CommonGatewayHelper需通过TransitionBehavior监控 +4. **委托模式** - 使用装饰器模式包装原有组件,保持原有功能的同时添加监控逻辑 +5. **职责分离** - 监控组件与业务组件解耦,便于维护和测试 + +### 推荐配置组合 + +```java +// 编程式配置(setter注入) +config.setDelegationExecutor(new MonitoringDelegationExecutor(config.getDelegationExecutor())); +config.setExpressionEvaluator(new MonitoringExpressionEvaluator(config.getExpressionEvaluator())); +config.setListenerExecutor(new MonitoringListenerExecutor(config.getListenerExecutor())); +config.setExceptionProcessor(new MonitoringExceptionProcessor()); +config.setVariablePersister(new MonitoringVariablePersister(config.getVariablePersister())); + +// @ExtensionBinding方式(网关决策监控) +// 添加 @ExtensionBinding 注解的 MonitoringSequenceFlowBehavior 类 +// 确保包扫描能发现此注解类 +``` diff --git a/docs/api-guide.md b/docs/api-guide.md new file mode 100644 index 000000000..b3e62b42e --- /dev/null +++ b/docs/api-guide.md @@ -0,0 +1,233 @@ +# API 指南:CQRS(start / signal / query)+ 幂等建议 + +SmartEngine 的对外权限集中在 `SmartEngine` 接口,它把服务分为两类: + +- **CommandService**:改变状态(start/signal/abort/complete…) +- **QueryService**:只读查询(find/get/list…) + +代码位置:`core/src/main/java/com/alibaba/smart/framework/engine/SmartEngine.java` + +--- + +## 1. SmartEngine 总入口 + +你在业务系统里通常会把 `SmartEngine` 作为单例注入,然后按需调用其子服务: + +- `getRepositoryCommandService()` +- `getProcessCommandService()` +- `getExecutionCommandService()` +- `getTaskCommandService()` +- `getVariableCommandService()` +- `get*QueryService()` + +## 2. Command API(按服务分组) + +下面根据源码接口自动整理(签名以 dev 分支为准): + +### Command · DeploymentCommandService + +- `DeploymentInstance createDeployment(CreateDeploymentCommand createDeploymentCommand) ;` +- `DeploymentInstance updateDeployment(UpdateDeploymentCommand updateDeploymentCommand) ;` +- `void inactivateDeploymentInstance(String deploymentInstanceId);` +- `void inactivateDeploymentInstance(String deploymentInstanceId,String tenantId);` +- `void activateDeploymentInstance(String deploymentInstanceId);` +- `void activateDeploymentInstance(String deploymentInstanceId,String tenantId);` +- `void deleteDeploymentInstanceLogically(String deploymentInstanceId);` +- `void deleteDeploymentInstanceLogically(String deploymentInstanceId,String tenantId);` + +### Command · ExecutionCommandService + +- `ProcessInstance signal(String executionInstanceId, Map request, Map response);` +- `ProcessInstance signal(String processInstanceId, String executionInstanceId, Map request, Map response);` +- `ProcessInstance signal(String executionInstanceId, Map request);` +- `ProcessInstance signal(String executionInstanceId);` +- `void markDone(String executionInstanceId);` +- `void markDone(String executionInstanceId,String tenantId);` +- `ProcessInstance jumpFrom(ProcessInstance processInstance,String activityId,String executionInstanceId, Map request);` +- `void retry(ProcessInstance processInstance, String activityId, ExecutionContext executionContext);` +- `ExecutionInstance createExecution(ActivityInstance activityInstance);` + +### Command · NotificationCommandService + +- `void markAsRead(String notificationId, String tenantId);` +- `void batchMarkAsRead(List notificationIds, String tenantId);` + +### Command · ProcessCommandService + +- `ProcessInstance start(String processDefinitionId, String processDefinitionVersion, Map request, Map response);` +- `ProcessInstance start(String processDefinitionId, String processDefinitionVersion, Map request);` +- `ProcessInstance start(String processDefinitionId, String processDefinitionVersion);` +- `ProcessInstance start(String processDefinitionId, String processDefinitionVersion,String tenantId);` +- `ProcessInstance startWith(String deploymentInstanceId, String userId, Map request, Map response);` +- `ProcessInstance startWith(String deploymentInstanceId, String userId, Map request);` +- `ProcessInstance startWith(String deploymentInstanceId, Map request);` +- `ProcessInstance startWith(String deploymentInstanceId);` +- `ProcessInstance startWith(String deploymentInstanceId,String tenantId);` +- `void abort(String processInstanceId);` +- `void abort(String processInstanceId,String tenantId);` +- `void abort(String processInstanceId, String reason,String tenantId);` +- `void abort(String processInstanceId, Map request);` + +### Command · RepositoryCommandService + +- `ProcessDefinitionSource deploy(String classPathResource) ;` +- `ProcessDefinitionSource deploy(String classPathResource,String tenantId) ;` +- `ProcessDefinitionSource deploy(InputStream inputStream) ;` +- `ProcessDefinitionSource deploy(InputStream inputStream,String tenantId) ;` +- `ProcessDefinitionSource deployWithUTF8Content(String uTF8ProcessDefinitionContent) ;` +- `ProcessDefinitionSource deployWithUTF8Content(String uTF8ProcessDefinitionContent,String tenantId) ;` + +### Command · SupervisionCommandService + +- `void closeSupervision(String supervisionId, String tenantId);` +- `void autoCloseSupervisionByTask(String taskInstanceId, String tenantId);` + +### Command · TaskCommandService + +- `ProcessInstance complete(String taskId, Map request);` +- `ProcessInstance complete(String taskId, String userId, Map request);` +- `ProcessInstance complete(String taskId, Map request, Map response);` +- `void transfer(String taskId, String fromUserId, String toUserId);` +- `void transfer(String taskId, String fromUserId, String toUserId,String tenantId);` +- `TaskInstance createTask(ExecutionInstance executionInstance, String taskInstanceStatus, Map request);` +- `void markDone(String taskId, Map request);` +- `void removeTaskAssigneeCandidate(String taskId,String tenantId, TaskAssigneeCandidateInstance taskAssigneeCandidateInstance);` +- `void addTaskAssigneeCandidate(String taskId,String tenantId, TaskAssigneeCandidateInstance taskAssigneeCandidateInstance);` +- `void transferWithReason(String taskId, String fromUserId, String toUserId, String reason, String tenantId);` +- `ProcessInstance rollbackTask(String taskId, String targetActivityId, String reason, String tenantId);` +- `void addTaskAssigneeCandidateWithReason(String taskId, String tenantId, TaskAssigneeCandidateInstance taskAssigneeCandidateInstance, String reason);` +- `void removeTaskAssigneeCandidateWithReason(String taskId, String tenantId, TaskAssigneeCandidateInstance taskAssigneeCandidateInstance, String reason);` + +### Command · VariableCommandService + +- `void insert(VariableInstance... variableInstance);` + +### Query · ActivityQueryService + +- `List findAll(String processInstanceId);` +- `List findAll(String processInstanceId,String tenantId);` + +### Query · DeploymentQueryService + +- `DeploymentInstance findById(String deploymentInstanceId);` +- `DeploymentInstance findById(String deploymentInstanceId,String tenantId);` +- `List findList(DeploymentInstanceQueryParam deploymentInstanceQueryParam) ;` +- `Integer count(DeploymentInstanceQueryParam deploymentInstanceQueryParam);` + +### Query · ExecutionQueryService + +- `List findActiveExecutionList(String processInstanceId,String tenantId);` +- `List findActiveExecutionList(String processInstanceId);` +- `List findAll(String processInstanceId);` +- `List findAll(String processInstanceId,String tenantId);` + +### Query · NotificationQueryService + +- `List findNotificationList(NotificationQueryParam param);` +- `Long countNotifications(NotificationQueryParam param);` +- `Long countUnreadNotifications(String receiverUserId, String tenantId);` +- `NotificationInstance findOne(String notificationId, String tenantId);` + +### Query · ProcessQueryService + +- `ProcessInstance findById(String processInstanceId);` +- `ProcessInstance findById(String processInstanceId,String tenantId);` +- `List findList(ProcessInstanceQueryParam processInstanceQueryParam);` +- `Long count(ProcessInstanceQueryParam processInstanceQueryParam);` +- `List findCompletedProcessList(CompletedProcessQueryParam param);` +- `Long countCompletedProcessList(CompletedProcessQueryParam param);` + +### Query · RepositoryQueryService + +- `ProcessDefinition getCachedProcessDefinition(String processDefinitionId, String version);` +- `ProcessDefinition getCachedProcessDefinition(String processDefinitionId, String version,String tenantId);` +- `ProcessDefinition getCachedProcessDefinition(String processDefinitionIdAndVersion);` +- `Collection getAllCachedProcessDefinition();` + +### Query · SupervisionQueryService + +- `List findSupervisionList(SupervisionQueryParam param);` +- `Long countSupervision(SupervisionQueryParam param);` +- `List findActiveSupervisionByTask(String taskInstanceId, String tenantId);` +- `SupervisionInstance findOne(String supervisionId, String tenantId);` + +### Query · TaskAssigneeQueryService + +- `List findList(String taskInstanceId);` +- `List findList(String taskInstanceId,String tenantId);` +- `Map> findAssigneeOfInstanceList(List taskInstanceIdList);` +- `Map> findAssigneeOfInstanceList(List taskInstanceIdList,String tenantId);` + +### Query · TaskQueryService + +- `List findPendingTaskList(PendingTaskQueryParam pendingTaskQueryParam);` +- `Long countPendingTaskList(PendingTaskQueryParam pendingTaskQueryParam);` +- `List findTaskListByAssignee(TaskInstanceQueryByAssigneeParam param);` +- `Long countTaskListByAssignee(TaskInstanceQueryByAssigneeParam param);` +- `List findAllPendingTaskList(String processInstanceId);` +- `List findAllPendingTaskList(String processInstanceId,String tenantId);` +- `TaskInstance findOne(String taskInstanceId);` +- `TaskInstance findOne(String taskInstanceId,String tenantId);` +- `List findList(TaskInstanceQueryParam taskInstanceQueryParam);` +- `Long count(TaskInstanceQueryParam taskInstanceQueryParam);` +- `List findCompletedTaskList(CompletedTaskQueryParam param);` +- `Long countCompletedTaskList(CompletedTaskQueryParam param);` + +### Query · VariableQueryService + +- `List findProcessInstanceVariableList(String processInstanceId);` +- `List findProcessInstanceVariableList(String processInstanceId,String tenantId);` +- `List findList(String processInstanceId, String executionInstanceId);` +- `List findList(String processInstanceId, String executionInstanceId,String tenantId);` + + +> 提示:同一个权限经常存在 tenantId 重载;多租户系统建议统一封装一层 Facade,把 tenantId 的注入与 request special key 的注入都收敛到入口。 + +## 3. Query API(常见查询路径) + +你最常用的查询通常是: + +1) 通过 `ProcessInstance` 查当前运行状态 +2) 通过 `ExecutionInstance` 找 active token(尤其是 receiveTask/并行场景) +3) 通过 `TaskInstance` 查待办与办理记录 +4) 通过 `VariableInstance` 查业务变量(或用于网关条件) + +### 3.1 Fluent Query API(推荐) + +**3.7 版本新增**。对于 Task、ProcessInstance、Supervision、Notification 的查询,推荐使用 Fluent Query API: + +```java +List tasks = smartEngine.createTaskQuery() + .taskAssignee(userId) + .taskStatus(status != null, status) + .orderByCreateTime().desc() + .listPage(0, 10); +``` + +完整文档:`03-usage/fluent-query-guide.md` + +## 4. 幂等建议(生产必做) + +SmartEngine 的 API 设计允许你在上层实现幂等(尤其是 start / signal / complete): + +- **start 幂等**:用业务唯一键(订单号、申请单号)做幂等 key +- **signal 幂等**:用事件 id(messageId)做幂等 key +- **complete 幂等**:用 taskInstanceId + 操作类型 做幂等 key + +实现方式建议: + +- 在你自己的数据库中建立幂等表(key -> result) +- start/signal/complete 前先查幂等表,已执行则直接返回结果 +- 幂等写入与引擎调用尽量放同一事务/同一最终一致性链路 + +> 引擎内部也有一些 request special key 可以承载扩展字段,但幂等更建议由业务层做明确表设计。 + +## 5. 事务边界建议 + +- Custom 模式:推荐把引擎推进与业务写入放在同一事务(你掌控存储接口) +- DataBase 模式:推荐用 Spring 事务包裹“引擎调用 + 业务写入”,并明确: + - 引擎写入失败时整体回滚 + - 业务写入失败时引擎回滚(避免产生“孤儿流程实例/任务”) + +详见:`04-persistence/storage-overview.md` + diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 000000000..538922d9a --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,89 @@ +# 架构:core / extension / docs 的模块划分与依赖关系 + +SmartEngine 仓库按 Maven 多模块组织(根 `pom.xml`): + +```text +- core +- extension/storage/storage-common +- extension/storage/storage-custom +- extension/storage/storage-mysql +- extension/retry/retry-common +- extension/retry/retry-custom +- extension/retry/retry-mysql +``` + + +--- + +## 1. core(引擎内核) + +目录:`core/` + +职责: + +- BPMN 解析(parser) +- 执行模型(process/execution/activity/token) +- 行为实现(serviceTask/receiveTask/gateway…) +- 对外服务接口(SmartEngine + Command/QueryService) +- 扩展机制(ExtensionBinding、AnnotationScanner、ExtensionContainer) +- 常量、工具、异常处理(ExceptionProcessor 等) + +依赖要求尽量轻(README 中强调 “Less Dependent / avoid JAR hell”)。 + +--- + +## 2. extension(可插拔扩展) + +目录:`extension/` + +### 2.1 storage(存储实现) + +- `storage-custom`:测试/示例存根,实现 Storage 接口但不落库 +- `storage-mysql`:DataBase 模式实现(MyBatis + DDL + SQLMap) + +### 2.2 retry(失败重试) + +- `retry-common`:注解与公共抽象(@Retryable 等) +- `retry-mysql`:重试记录的关系库存储(含 DDL) + +--- + +## 3. ecology(生态/工具) + +目录:`ecology/` + +仓库中包含一些与“建模/设计器/适配”相关的代码(例如 designer),通常不影响 core 的运行期语义,但对平台化落地有价值(例如你做低代码建模器时可以复用其中的结构)。 + +--- + +## 4. 依赖方向(建议) + +```mermaid +flowchart TB + core --> extension_storage_custom + core --> extension_storage_mysql + core --> extension_retry_common + extension_retry_mysql --> extension_retry_common + extension_storage_mysql --> core + extension_storage_custom --> core +``` + +原则: + +- core 不依赖具体存储 +- extension 依赖 core +- 不要在 core 中引入强耦合框架(保持可嵌入性) + +--- + +## 5. 你做二开时的推荐分层 + +如果你要做“SmartEngine + 业务平台”的深度集成,建议你在业务仓库中按以下分层组织: + +- `engine-adapter`:对 SmartEngine 的封装(tenant、幂等、审计、监控) +- `engine-storage-impl`:自研 Storage(支持多 DB/历史/归档) +- `engine-user-integration`:组织/角色/权限/待办查询 read model +- `engine-ops`:监控、告警、数据清理、压测脚本 + +这样你可以保持引擎内核的可升级性,同时把“平台特有权限”外置。 + diff --git a/docs/behaviors.md b/docs/behaviors.md new file mode 100644 index 000000000..b20c48058 --- /dev/null +++ b/docs/behaviors.md @@ -0,0 +1,66 @@ +# 节点行为扩展:ServiceTask / ReceiveTask / 网关… + +SmartEngine 的执行核心抽象是 `ActivityBehavior`: + +- 每种 BPMN 节点类型对应一个 behavior +- behavior 可以通过扩展机制替换/覆盖 + +--- + +## 1. 内置行为在哪? + +- BPMN 行为(事件、网关、sequence flow 等):`core/.../engine/bpmn/behavior/*` +- Task 行为(ServiceTask/ReceiveTask/UserTask 等):`core/.../engine/behavior/impl/*` + +--- + +## 2. 如何覆盖某类节点行为? + +### 2.1 使用 ExtensionBinding 绑定 + +- group:`ExtensionConstant.ACTIVITY_BEHAVIOR` +- bindKey:节点类型(例如 serviceTask) + +实现类示意: + +```java +@ExtensionBinding(group = ExtensionConstant.ACTIVITY_BEHAVIOR, bindKey = "serviceTask", priority = 100) +public class YourServiceTaskBehavior extends ServiceTaskBehavior { + @Override + public void execute(...) { ... } +} +``` + +### 2.2 你应该优先“组合”而不是“重写全部” + +常见推荐做法: + +- 在执行前后加埋点、权限校验、幂等校验 +- 委派调用仍复用默认 DelegationExecutor +- 异常仍交给 ExceptionProcessor + +--- + +## 3. Delegation(业务委派)怎么对接? + +ServiceTask 等节点通常最终会走 `DelegationExecutor`: + +- 解析阶段:从 smart:class / 其他扩展属性得到 class/beanName +- 执行阶段:通过 InstanceAccessor 获取实例并调用 + +你可以替换: + +- `InstanceAccessor`(决定如何拿实例) +- `DelegationExecutor`(决定如何调用、是否异步、是否熔断) + +--- + +## 4. 网关行为扩展注意事项 + +- Exclusive/Inclusive/Parallel 的语义复杂,建议尽量不要“重写语义” +- 如果你确实要扩展: + - 建议只在“条件表达式求值、并发控制、线程池选择”层面做增强 + - 并配套跑通仓库 gateway/tree 的测试 + +并发专题见:`05-extensibility/concurrency.md` + diff --git a/docs/build-and-test.md b/docs/build-and-test.md new file mode 100644 index 000000000..01a76518b --- /dev/null +++ b/docs/build-and-test.md @@ -0,0 +1,61 @@ +# 构建、单测、集成测试、测试数据/用例结构 + +--- + +## 1. 构建 + +在仓库根目录执行: + +```bash +mvn -q -DskipTests=false test +``` + +按模块构建: + +```bash +mvn -pl core -am test +mvn -pl extension/storage/storage-custom -am test +mvn -pl extension/storage/storage-mysql -am test +``` + +> 提示:DataBase 模式测试通常需要本地数据库(默认配置在 `storage-mysql/src/test/resources/spring/application-test.xml`)。 + +--- + +## 2. 单元测试 vs 集成测试 + +- core:以 parser/behavior 的单元测试为主(不依赖数据库) +- storage-custom:以“语义回归测试”为主(并行网关、跳转等) +- storage-mysql:以“存储 + MyBatis + 事务”的集成测试为主(依赖数据库) + +--- + +## 3. 测试资源(BPMN) + +常见目录: + +- `core/src/test/resources/process-def/*` +- `extension/storage/storage-custom/src/test/resources/*.xml` +- `extension/storage/storage-mysql/src/test/resources/*.bpmn20.xml` + +建议你把这些 BPMN 当作“语义金标准”,二开时新增行为/解析器要新增对应 BPMN + 测试。 + +--- + +## 4. 测试数据库准备(DataBase 模式) + +仓库未提供 docker-compose,但你可以用本地 PostgreSQL / MySQL: + +1) 创建数据库与用户 +2) 执行 DDL(见 `04-persistence/database-schema.md`) +3) 修改 `application-test.xml` 的 JDBC URL/账号密码 +4) 跑 `storage-mysql` 模块测试 + +--- + +## 5. CI 建议(如果你要在团队里用) + +- MySQL + PostgreSQL 双跑(避免方言回归) +- 并行网关相关测试要单独作为 “语义回归” job +- 对关键 SQL 做 explain 基线(防止性能回归) + diff --git a/docs/concurrency.md b/docs/concurrency.md new file mode 100644 index 000000000..7a4f99842 --- /dev/null +++ b/docs/concurrency.md @@ -0,0 +1,68 @@ +# 并发:ParallelGateway 的 LockStrategy / ExecutorService + +并行网关是 SmartEngine 最“工程化”的部分之一:它既是 BPMN 语义,又会在生产里遇到真实的并发问题。 + +本章解释: + +- 并行分叉/汇聚时执行实例如何变化 +- 引擎如何选择线程池、如何等待、如何避免重复推进 +- 你应该如何在集群/多线程下配置与扩展 + +--- + +## 1. ParallelGatewayUtil:并行的“配置解释器” + +关键类:`core/.../util/ParallelGatewayUtil.java` + +它负责从节点 properties 中解析: + +- 使用哪个 ExecutorService(默认或指定 poolName) +- latch 的等待时间 +- 并发相关的开关与参数 + +意味着:你可以在 BPMN 中通过扩展属性,为不同并行网关指定不同的线程池策略。 + +--- + +## 2. ExecutorService poolsMap:多线程池治理 + +`ProcessEngineConfiguration` 中存在: + +- `Map getExecutorServiceMap()`(或同等配置项) + +常见策略: + +- 默认线程池:用于大多数并行网关 +- 专用线程池:用于“重任务”分支(外部调用、IO、耗时计算) +- 限流:通过线程池大小/队列大小控制系统背压 + +--- + +## 3. LockStrategy(Deprecated)还能用吗? + +`LockStrategy` 接口在源码中标记为 Deprecated,原因通常是: + +- 锁粒度太细或语义不清 +- 在集群环境下无法保证正确性(需要分布式锁/事务锁) + +建议: + +- 不再依赖 LockStrategy 做核心正确性 +- 采用数据库层面的行锁/乐观锁(DataBase 模式) +- 或采用“幂等推进 + 去重”(Custom 模式) + +--- + +## 4. 生产建议(最重要) + +- 并行网关 join 的正确性优先于性能 +- 任何并行推进都要有幂等保障(避免重复执行业务 side effect) +- 外部调用建议: + - 幂等 + 重试 + 熔断 + - 失败可补偿(compensation) + +仓库里并行相关的测试非常多,建议你把它们当作“语义回归集”: + +- storage-custom:ConcurrentParallelGatewayTest 等 +- storage-mysql:ConcurrentParallelGatewayDBTest 等 + diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 000000000..692591464 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,21 @@ +# 贡献指南(Contributing) + +本文件是团队内贡献的补充说明;以仓库根目录 `CONTRIBUTING.md` 为准。 + +## 代码与架构原则 + +- 保持 `core` 依赖轻量,不引入重型框架。 +- 新能力优先通过扩展点实现,避免直接修改核心语义。 +- 行为或解析变更必须附带测试与示例 BPMN。 + +## PR 要求 + +- PR 描述必须写明:背景、方案、兼容性风险、回滚方式、测试结果。 +- 涉及数据库变更时必须同步更新 MySQL/PostgreSQL DDL 与 SQLMap,并提供迁移说明。 +- 对外行为变化必须同步更新 docs 与变更日志。 + +## 回归测试建议 + +- `core`:parser/behavior 全量测试。 +- `storage-custom`:并行网关、跳转等语义回归。 +- `storage-mysql`:流程启动、任务、变量、并行网关等集成路径。 diff --git a/docs/database-schema.md b/docs/database-schema.md new file mode 100644 index 000000000..912f28f8f --- /dev/null +++ b/docs/database-schema.md @@ -0,0 +1,606 @@ +# DataBase 模式表结构/索引/清理策略 + +本章包含 3 部分: + +1) SmartEngine 核心表(process/execution/activity/task/variable/deployment) +2) 工作流增强表(通知/督办/转派/回退等) +3) 索引与清理策略建议 + +--- + +## 1. 核心表(DDL) + +### 1.1 MySQL 版(schema.sql) + +```sql + + +CREATE TABLE `se_deployment_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK' , + `gmt_create` datetime(6) NOT NULL COMMENT 'create time' , + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time' , + `process_definition_id` varchar(255) NOT NULL COMMENT 'process definition id' , + `process_definition_version` varchar(255) DEFAULT NULL COMMENT 'process definition version' , + `process_definition_type` varchar(255) DEFAULT NULL COMMENT 'process definition type' , + `process_definition_code` varchar(255) DEFAULT NULL COMMENT 'process definition code' , + `process_definition_name` varchar(255) DEFAULT NULL COMMENT 'process definition name' , + `process_definition_desc` varchar(255) DEFAULT NULL COMMENT 'process definition desc' , + `process_definition_content` mediumtext NOT NULL COMMENT 'process definition content' , + `deployment_user_id` varchar(128) NOT NULL COMMENT 'deployment user id' , + `deployment_status` varchar(64) NOT NULL COMMENT 'deployment status' , + `logic_status` varchar(64) NOT NULL COMMENT 'logic status' , + `extension` mediumtext DEFAULT NULL COMMENT 'extension' , + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id' , + + PRIMARY KEY (`id`) +) ; + +CREATE TABLE `se_process_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK' , + `gmt_create` datetime(6) NOT NULL COMMENT 'create time' , + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time' , + `process_definition_id_and_version` varchar(128) NOT NULL COMMENT 'process definition id and version' , + `process_definition_type` varchar(255) DEFAULT NULL COMMENT 'process definition type' , + `status` varchar(64) NOT NULL COMMENT ' 1.running 2.completed 3.aborted', + `parent_process_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'parent process instance id' , + `parent_execution_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'parent execution instance id' , + `start_user_id` varchar(128) DEFAULT NULL COMMENT 'start user id' , + `biz_unique_id` varchar(255) DEFAULT NULL COMMENT 'biz unique id' , + `reason` varchar(255) DEFAULT NULL COMMENT 'reason' , + `comment` varchar(255) DEFAULT NULL COMMENT 'comment' , + `title` varchar(255) DEFAULT NULL COMMENT 'title' , + `tag` varchar(255) DEFAULT NULL COMMENT 'tag' , + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id' , + + PRIMARY KEY (`id`) +) ; + +CREATE TABLE `se_activity_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK' , + `gmt_create` datetime(6) NOT NULL COMMENT 'create time' , + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time' , + `process_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'process instance id' , + `process_definition_id_and_version` varchar(255) NOT NULL COMMENT 'process definition id and version' , + `process_definition_activity_id` varchar(64) NOT NULL COMMENT 'process definition activity id' , + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id' , + PRIMARY KEY (`id`) +) ; + +CREATE TABLE `se_task_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK' , + `gmt_create` datetime(6) NOT NULL COMMENT 'create time' , + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time' , + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id' , + `process_definition_id_and_version` varchar(128) DEFAULT NULL COMMENT 'process definition id and version' , + `process_definition_type` varchar(255) DEFAULT NULL COMMENT 'process definition type' , + `activity_instance_id` bigint(20) unsigned NOT NULL COMMENT 'activity instance id' , + `process_definition_activity_id` varchar(255) NOT NULL COMMENT 'process definition activity id' , + `execution_instance_id` bigint(20) unsigned NOT NULL COMMENT 'execution instance id' , + `claim_user_id` varchar(255) DEFAULT NULL COMMENT 'claim user id' , + `title` varchar(255) DEFAULT NULL COMMENT 'title' , + `priority` int(11) DEFAULT 500 COMMENT 'priority' , + `tag` varchar(255) DEFAULT NULL COMMENT 'tag' , + `claim_time` datetime(6) DEFAULT NULL COMMENT 'claim time' , + `complete_time` datetime(6) DEFAULT NULL COMMENT 'complete time' , + `status` varchar(255) NOT NULL COMMENT 'status' , + `comment` varchar(255) DEFAULT NULL COMMENT 'comment' , + `extension` varchar(255) DEFAULT NULL COMMENT 'extension' , + `domain_code` varchar(64) DEFAULT NULL COMMENT 'domain code' , + `extra` json DEFAULT NULL COMMENT 'extra JSON data' , + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id' , + + PRIMARY KEY (`id`) +) ; + +CREATE TABLE `se_execution_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK' , + `gmt_create` datetime(6) NOT NULL COMMENT 'create time' , + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time' , + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id' , + `process_definition_id_and_version` varchar(255) NOT NULL COMMENT 'process definition id and version' , + `process_definition_activity_id` varchar(255) NOT NULL COMMENT 'process definition activity id' , + `activity_instance_id` bigint(20) unsigned NOT NULL COMMENT 'activity instance id' , + `block_id` bigint(20) unsigned DEFAULT NULL COMMENT 'block_id' , + `active` tinyint(4) NOT NULL COMMENT '1:active 0:inactive', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id' , + PRIMARY KEY (`id`) +) ; + + +CREATE TABLE `se_task_assignee_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK' , + `gmt_create` datetime(6) NOT NULL COMMENT 'create time' , + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time' , + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id' , + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id' , + `assignee_id` varchar(255) NOT NULL COMMENT 'assignee id' , + `assignee_type` varchar(128) NOT NULL COMMENT 'assignee type' , + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id' , + PRIMARY KEY (`id`) +) ; + + +CREATE TABLE `se_variable_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK' , + `gmt_create` datetime(6) NOT NULL COMMENT 'create time' , + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time' , + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id' , + `execution_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'execution instance id' , + `field_key` varchar(128) NOT NULL COMMENT 'field key' , + `field_type` varchar(128) NOT NULL COMMENT 'field type' , + `field_double_value` decimal(65,30) DEFAULT NULL COMMENT 'field double value' , + `field_long_value` bigint(20) DEFAULT NULL COMMENT 'field long value' , + `field_string_value` varchar(4000) DEFAULT NULL COMMENT 'field string value' , + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id' , + + PRIMARY KEY (`id`) +) ; +``` + + +### 1.2 PostgreSQL 版(schema-postgre.sql) + +```sql +CREATE TABLE se_deployment_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_definition_id varchar(255) NOT NULL, + process_definition_version varchar(255), + process_definition_type varchar(255), + process_definition_code varchar(255), + process_definition_name varchar(255), + process_definition_desc varchar(255), + process_definition_content text NOT NULL, + deployment_user_id varchar(128) NOT NULL, + deployment_status varchar(64) NOT NULL, + logic_status varchar(64) NOT NULL, + extension text, + tenant_id varchar(64), + PRIMARY KEY (id) +); + +CREATE TABLE se_process_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_definition_id_and_version varchar(128) NOT NULL, + process_definition_type varchar(255), + status varchar(64) NOT NULL, + parent_process_instance_id bigint, + parent_execution_instance_id bigint, + start_user_id varchar(128), + biz_unique_id varchar(255), + reason varchar(255), + comment varchar(255), + title varchar(255), + tag varchar(255), + tenant_id varchar(64), + PRIMARY KEY (id) +); + +CREATE TABLE se_activity_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint, + process_definition_id_and_version varchar(255) NOT NULL, + process_definition_activity_id varchar(64) NOT NULL, + tenant_id varchar(64), + PRIMARY KEY (id) +); + +CREATE TABLE se_task_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + process_definition_id_and_version varchar(128), + process_definition_type varchar(255), + activity_instance_id bigint NOT NULL, + process_definition_activity_id varchar(255) NOT NULL, + execution_instance_id bigint NOT NULL, + claim_user_id varchar(255), + title varchar(255), + priority int DEFAULT 500, + tag varchar(255), + claim_time timestamp(6), + complete_time timestamp(6), + status varchar(255) NOT NULL, + comment varchar(255), + extension varchar(255), + domain_code varchar(64) DEFAULT NULL, + extra jsonb DEFAULT NULL, + tenant_id varchar(64), + PRIMARY KEY (id) +); + +CREATE TABLE se_execution_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + process_definition_id_and_version varchar(255) NOT NULL, + process_definition_activity_id varchar(255) NOT NULL, + activity_instance_id bigint NOT NULL, + block_id bigint, + active smallint NOT NULL, + tenant_id varchar(64), + PRIMARY KEY (id) +); + +CREATE TABLE se_task_assignee_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint NOT NULL, + assignee_id varchar(255) NOT NULL, + assignee_type varchar(128) NOT NULL, + tenant_id varchar(64), + PRIMARY KEY (id) +); + +CREATE TABLE se_variable_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + execution_instance_id bigint, + field_key varchar(128) NOT NULL, + field_type varchar(128) NOT NULL, + field_double_value decimal(65,30), + field_long_value bigint, + field_string_value varchar(4000), + tenant_id varchar(64), + PRIMARY KEY (id) +); +``` + + +--- + +## 2. 工作流增强表(DDL) + +### 2.1 MySQL(workflow-enhancement-schema-mysql.sql) + +```sql +-- 工作流管理系统增强功能数据库表结构 +-- 基于SmartEngine现有表结构设计规范 + +-- 督办记录表 +CREATE TABLE `se_supervision_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `supervisor_user_id` varchar(255) NOT NULL COMMENT 'supervisor user id', + `supervision_reason` varchar(500) DEFAULT NULL COMMENT 'supervision reason', + `supervision_type` varchar(64) NOT NULL COMMENT 'supervision type: urge/track/remind', + `status` varchar(64) NOT NULL COMMENT 'status: active/closed', + `close_time` datetime(6) DEFAULT NULL COMMENT 'close time', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`), + KEY `idx_process_instance_id` (`process_instance_id`), + KEY `idx_task_instance_id` (`task_instance_id`), + KEY `idx_supervisor_user_id` (`supervisor_user_id`), + KEY `idx_status` (`status`), + KEY `idx_tenant_id` (`tenant_id`) +); + +-- 知会抄送表 +CREATE TABLE `se_notification_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'task instance id', + `sender_user_id` varchar(255) NOT NULL COMMENT 'sender user id', + `receiver_user_id` varchar(255) NOT NULL COMMENT 'receiver user id', + `notification_type` varchar(64) NOT NULL COMMENT 'notification type: cc/inform', + `title` varchar(255) DEFAULT NULL COMMENT 'notification title', + `content` varchar(1000) DEFAULT NULL COMMENT 'notification content', + `read_status` varchar(64) NOT NULL DEFAULT 'unread' COMMENT 'read status: unread/read', + `read_time` datetime(6) DEFAULT NULL COMMENT 'read time', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`), + KEY `idx_process_instance_id` (`process_instance_id`), + KEY `idx_task_instance_id` (`task_instance_id`), + KEY `idx_sender_user_id` (`sender_user_id`), + KEY `idx_receiver_user_id` (`receiver_user_id`), + KEY `idx_read_status` (`read_status`), + KEY `idx_tenant_id` (`tenant_id`) +); + +-- 任务移交记录表 +CREATE TABLE `se_task_transfer_record` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `from_user_id` varchar(255) NOT NULL COMMENT 'from user id', + `to_user_id` varchar(255) NOT NULL COMMENT 'to user id', + `transfer_reason` varchar(500) DEFAULT NULL COMMENT 'transfer reason', + `deadline` datetime(6) DEFAULT NULL COMMENT 'processing deadline', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`), + KEY `idx_task_instance_id` (`task_instance_id`), + KEY `idx_from_user_id` (`from_user_id`), + KEY `idx_to_user_id` (`to_user_id`), + KEY `idx_tenant_id` (`tenant_id`) +); + +-- 加签减签操作记录表 +CREATE TABLE `se_assignee_operation_record` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `operation_type` varchar(64) NOT NULL COMMENT 'operation type: add_assignee/remove_assignee', + `operator_user_id` varchar(255) NOT NULL COMMENT 'operator user id', + `target_user_id` varchar(255) NOT NULL COMMENT 'target user id', + `operation_reason` varchar(500) DEFAULT NULL COMMENT 'operation reason', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`), + KEY `idx_task_instance_id` (`task_instance_id`), + KEY `idx_operation_type` (`operation_type`), + KEY `idx_operator_user_id` (`operator_user_id`), + KEY `idx_target_user_id` (`target_user_id`), + KEY `idx_tenant_id` (`tenant_id`) +); + +-- 流程回退记录表 +CREATE TABLE `se_process_rollback_record` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `rollback_type` varchar(64) NOT NULL COMMENT 'rollback type: previous/specific', + `from_activity_id` varchar(255) NOT NULL COMMENT 'from activity id', + `to_activity_id` varchar(255) NOT NULL COMMENT 'to activity id', + `operator_user_id` varchar(255) NOT NULL COMMENT 'operator user id', + `rollback_reason` varchar(500) DEFAULT NULL COMMENT 'rollback reason', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`), + KEY `idx_process_instance_id` (`process_instance_id`), + KEY `idx_task_instance_id` (`task_instance_id`), + KEY `idx_rollback_type` (`rollback_type`), + KEY `idx_operator_user_id` (`operator_user_id`), + KEY `idx_tenant_id` (`tenant_id`) +); +``` + + +### 2.2 PostgreSQL(workflow-enhancement-schema-postgresql.sql) + +```sql +CREATE TABLE se_supervision_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint NOT NULL, + supervisor_user_id varchar(255) NOT NULL, + supervision_reason varchar(500), + supervision_type varchar(64) NOT NULL, + status varchar(64) NOT NULL, + close_time timestamp(6), + tenant_id varchar(64) +); + +CREATE INDEX idx_supervision_process_instance_id + ON se_supervision_instance (process_instance_id); + +CREATE INDEX idx_supervision_task_instance_id + ON se_supervision_instance (task_instance_id); + +CREATE INDEX idx_supervision_supervisor_user_id + ON se_supervision_instance (supervisor_user_id); + +CREATE INDEX idx_supervision_status + ON se_supervision_instance (status); + +CREATE INDEX idx_supervision_tenant_id + ON se_supervision_instance (tenant_id); + +COMMENT ON TABLE se_supervision_instance IS '督办记录表'; +COMMENT ON COLUMN se_supervision_instance.supervision_type IS 'urge/track/remind'; +COMMENT ON COLUMN se_supervision_instance.status IS 'active/closed'; + + + +CREATE TABLE se_notification_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint, + sender_user_id varchar(255) NOT NULL, + receiver_user_id varchar(255) NOT NULL, + notification_type varchar(64) NOT NULL, + title varchar(255), + content varchar(1000), + read_status varchar(64) NOT NULL DEFAULT 'unread', + read_time timestamp(6), + tenant_id varchar(64) +); + +CREATE INDEX idx_notification_process_instance_id + ON se_notification_instance (process_instance_id); + +CREATE INDEX idx_notification_task_instance_id + ON se_notification_instance (task_instance_id); + +CREATE INDEX idx_notification_sender_user_id + ON se_notification_instance (sender_user_id); + +CREATE INDEX idx_notification_receiver_user_id + ON se_notification_instance (receiver_user_id); + +CREATE INDEX idx_notification_read_status + ON se_notification_instance (read_status); + +CREATE INDEX idx_notification_tenant_id + ON se_notification_instance (tenant_id); + +COMMENT ON TABLE se_notification_instance IS '知会/抄送记录表'; +COMMENT ON COLUMN se_notification_instance.notification_type IS 'cc/inform'; +COMMENT ON COLUMN se_notification_instance.read_status IS 'unread/read'; + +CREATE TABLE se_assignee_operation_record ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + task_instance_id bigint NOT NULL, + operation_type varchar(64) NOT NULL, + operator_user_id varchar(255) NOT NULL, + target_user_id varchar(255) NOT NULL, + operation_reason varchar(500), + tenant_id varchar(64) +); + +CREATE INDEX idx_assignee_op_task_instance_id + ON se_assignee_operation_record (task_instance_id); + +CREATE INDEX idx_assignee_op_operation_type + ON se_assignee_operation_record (operation_type); + +CREATE INDEX idx_assignee_op_operator_user_id + ON se_assignee_operation_record (operator_user_id); + +CREATE INDEX idx_assignee_op_target_user_id + ON se_assignee_operation_record (target_user_id); + +CREATE INDEX idx_assignee_op_tenant_id + ON se_assignee_operation_record (tenant_id); + +COMMENT ON TABLE se_assignee_operation_record IS '加签/减签操作记录表'; +COMMENT ON COLUMN se_assignee_operation_record.operation_type + IS 'add_assignee/remove_assignee'; + + + + +CREATE TABLE se_task_transfer_record ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + task_instance_id bigint NOT NULL, + from_user_id varchar(255) NOT NULL, + to_user_id varchar(255) NOT NULL, + transfer_reason varchar(500), + deadline timestamp(6), + tenant_id varchar(64) +); + +CREATE INDEX idx_task_transfer_task_instance_id + ON se_task_transfer_record (task_instance_id); + +CREATE INDEX idx_task_transfer_from_user_id + ON se_task_transfer_record (from_user_id); + +CREATE INDEX idx_task_transfer_to_user_id + ON se_task_transfer_record (to_user_id); + +CREATE INDEX idx_task_transfer_tenant_id + ON se_task_transfer_record (tenant_id); + +COMMENT ON TABLE se_task_transfer_record IS '任务移交记录表'; + + +CREATE TABLE se_process_rollback_record ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint NOT NULL, + rollback_type varchar(64) NOT NULL, + from_activity_id varchar(255) NOT NULL, + to_activity_id varchar(255) NOT NULL, + operator_user_id varchar(255) NOT NULL, + rollback_reason varchar(500), + tenant_id varchar(64) +); + +CREATE INDEX idx_rollback_process_instance_id + ON se_process_rollback_record (process_instance_id); + +CREATE INDEX idx_rollback_task_instance_id + ON se_process_rollback_record (task_instance_id); + +CREATE INDEX idx_rollback_type + ON se_process_rollback_record (rollback_type); + +CREATE INDEX idx_rollback_operator_user_id + ON se_process_rollback_record (operator_user_id); + +CREATE INDEX idx_rollback_tenant_id + ON se_process_rollback_record (tenant_id); + +COMMENT ON TABLE se_process_rollback_record IS '流程回退记录表'; +COMMENT ON COLUMN se_process_rollback_record.rollback_type + IS 'previous/specific'; +``` + + +--- + +## 3. 索引(index.sql) + +```sql + + +alter table `se_execution_instance` + add key `idx_tenant_id_process_instance_id_and_status_tenant_id` (tenant_id,process_instance_id,active), + add key `idx_tenant_id_process_instance_id_and_activity_instance_id` (tenant_id,process_instance_id,activity_instance_id); + +alter table `se_task_assignee_instance` + add key `idx_tenant_id_task_instance_id` (tenant_id,task_instance_id), + add key `idx_tenant_id_assignee_id_and_type` (tenant_id,assignee_id,assignee_type); + +alter table `se_activity_instance` + add key `idx_tenant_id_process_instance_id` (tenant_id,process_instance_id); + +alter table `se_deployment_instance` + add key `idx_tenant_id_user_id_and_logic_status` (tenant_id,logic_status,deployment_user_id); + +alter table `se_process_instance` + add key `idx_tenant_id_start_user_id` (tenant_id,start_user_id), + add key `idx_tenant_id_status` (tenant_id,status); + +alter table `se_task_instance` + add key `idx_tenant_id_status` (tenant_id,status), + add key `idx_tenant_id_process_instance_id_and_status` (tenant_id,process_instance_id,status), + add key `idx_tenant_id_process_definition_type` (tenant_id,process_definition_type), + add key `idx_tenant_id_process_instance_id` (tenant_id,process_instance_id), + add key `idx_tenant_id_claim_user_id` (tenant_id,claim_user_id), + add key `idx_tenant_id_tag` (tenant_id,tag), + add key `idx_tenant_id_activity_instance_id` (tenant_id,activity_instance_id), + add key `idx_tenant_id_process_definition_activity_id` (tenant_id,process_definition_activity_id); + +alter table `se_variable_instance` + add key `idx_tenant_id_process_instance_id_and_execution_instance_id` (tenant_id,process_instance_id,execution_instance_id); +``` + + +--- + +## 4. 清理策略(建议) + +SmartEngine 默认表结构没有单独的“历史表”体系,常见做法是: + +- 使用 `status` 字段区分 active/closed(或 deleted) +- 定期清理: + - 已完成且超过保留期的 process/execution/activity/task/variable + - 已关闭的通知/督办/转派/回退记录 + +建议你在业务系统侧明确: + +- 保留期(如 90/180/365 天) +- 清理批大小(避免大事务) +- 清理顺序(先 task/assignee/variable,再 activity/execution,再 process/deployment) + +如果你需要更完善的审计/回放权限,建议自建 history 表或引入事件日志(event sourcing),并把引擎推进过程输出为可重放事件。 + diff --git a/docs/design/query-api-and-multi-db-design.md b/docs/design/query-api-and-multi-db-design.md new file mode 100644 index 000000000..815ed9093 --- /dev/null +++ b/docs/design/query-api-and-multi-db-design.md @@ -0,0 +1,233 @@ +# Smart Engine 查询 API 改进 & 多数据库支持设计文档 + +> 版本: 1.0 | 日期: 2026-02-07 + +--- + +## 一、背景与目标 + +### 1.1 当前问题 + +1. **查询 API 不够友好**:虽然已有 Fluent Query API(Activiti 风格),但缺少动态条件、共享接口、OR 条件等能力 +2. **多数据库支持不完整**:方言体系(8 种)已建立,但仅 MySQL/PostgreSQL/H2 有 schema 脚本,信创数据库缺失 +3. **DDL 维护成本高**:每种数据库独立维护 SQL 脚本,容易漏改 + +### 1.2 设计目标 + +- 查询 API 增强,向下兼容现有 API +- 覆盖 H2/MySQL/PostgreSQL + 信创(达梦/人大金仓/OceanBase) +- 最小化 DDL 维护成本 + +### 1.3 参考框架 + +| 框架 | 借鉴点 | +|------|--------| +| Activiti | 领域专用查询接口 `Query` — **已采用** | +| Flowable | 共享接口层次 `TaskInfoQuery` — **待引入** | +| MyBatis Plus | 动态条件 `eq(condition, col, val)` — **待引入** | + +--- + +## 二、查询 API 改进设计 + +### 2.1 P0:动态条件(Conditional Filtering) + +**问题**:调用方需要手动 if-else 判断是否附加条件 + +```java +// Before: 繁琐的 if-else +TaskQuery q = engine.createTaskQuery(); +if (userId != null) q.taskAssignee(userId); +if (status != null) q.taskStatus(status); +List result = q.list(); +``` + +**方案**:每个条件方法增加 `(boolean condition, value)` 重载 + +```java +// After: 一行搞定 +List result = engine.createTaskQuery() + .taskAssignee(userId != null, userId) + .taskStatus(status != null, status) + .list(); +``` + +**实现要点**: +- 在 `Query` 基础接口增加 `tenantId(boolean condition, String tenantId)` 示例 +- 各子接口增加对应重载方法 +- Impl 中,当 `condition == false` 时直接返回 `self()`,不设置字段 + +**向下兼容**:纯新增方法,不修改任何已有方法签名。 + +### 2.2 P1:共享接口层次(Shared Query Interface) + +**问题**:`TaskQuery`、`SupervisionQuery`、`NotificationQuery`、`ProcessInstanceQuery` 有大量重复方法: +- `processInstanceId(String)` +- `processInstanceIdIn(List)` +- `tenantId(String)` +- `orderByCreateTime()` / `orderByModifyTime()` +- `asc()` / `desc()` +- `pageOffset()` / `pageSize()` + +**方案**:抽取 `ProcessBoundQuery` 共享接口 + +```java +public interface ProcessBoundQuery, T> + extends Query { + + Q processInstanceId(String processInstanceId); + Q processInstanceIdIn(List processInstanceIds); + + // Conditional overloads + Q processInstanceId(boolean condition, String processInstanceId); + + // Common ordering + Q orderByCreateTime(); + Q orderByModifyTime(); + Q asc(); + Q desc(); +} +``` + +**继承关系**: + +``` +Query + └── ProcessBoundQuery (新增: 共享 processInstanceId, ordering) + ├── TaskQuery (领域方法: taskAssignee, taskStatus...) + ├── SupervisionQuery (领域方法: supervisorUserId, supervisionStatus...) + └── NotificationQuery (领域方法: receiverUserId, readStatus...) + └── ProcessInstanceQuery (独立: 查询主体就是 ProcessInstance) +``` + +**AbstractProcessBoundQuery**:对应 Impl 基类,包含 processInstanceId/排序的公共实现。 + +**向下兼容**:接口新增中间层,子接口继承关系变化但方法签名不变。 + +### 2.3 P2:taskStatusIn 多值条件 + +**问题**:当前 `taskStatus(String)` 只能传一个值 + +```java +// 想查 PENDING 和 RUNNING 的任务,目前做不到 +``` + +**方案**:增加 `xxxIn` 方法 + +```java +TaskQuery taskStatusIn(List statuses); +TaskQuery taskStatusIn(String... statuses); +``` + +### 2.4 不做的事(暂缓) + +- **OR 条件**:需要 MyBatis XML 层大量改造,业务需求不明确,暂不实现 +- **NativeQuery**:用户可直接用 MyBatis Mapper,引擎内不重复造轮子 + +--- + +## 三、多数据库 DDL 策略 + +### 3.1 核心原则 + +> **逻辑模型统一,DDL 语法适配** + +所有数据库的表结构(列名、列语义、索引策略)完全一致,仅 DDL 语法因数据库不同而异。 + +### 3.2 数据库差异分析 + +smart-engine 的表结构非常简单,真正的跨数据库差异只有 3 个点: + +| 差异点 | MySQL | PostgreSQL | Oracle/达梦/人大金仓 | H2 | +|--------|-------|-----------|---------------------|-----| +| **ID 生成** | `AUTO_INCREMENT` | `GENERATED BY DEFAULT AS IDENTITY` | `SEQUENCE` | `AUTO_INCREMENT` | +| **时间类型** | `datetime(6)` | `timestamp(6)` | `timestamp(6)` | `datetime(6)` | +| **大文本** | `mediumtext` | `text` | `clob` | `text` | +| **布尔/短整型** | `tinyint(4)` | `smallint` | `number(1)` | `tinyint` | +| **标识符引用** | `` ` `` | `"` | `"` | `` ` `` | + +### 3.3 数据库族群策略(3 套覆盖全部) + +| 族群 | 覆盖数据库 | 基础 schema | +|------|-----------|------------| +| **MySQL 系** | MySQL, MariaDB, OceanBase, TiDB, GBase | `schema.sql` (已有) | +| **PostgreSQL 系** | PostgreSQL, 人大金仓(KingBase), CockroachDB | `schema-postgresql.sql` (已有) | +| **Oracle 系** | Oracle, 达梦(DM) | `schema-oracle.sql` (**需新增**) | +| **H2** | H2 (测试) | `schema-h2.sql` (已有) | + +### 3.4 Oracle 系 Schema 设计 + +```sql +-- Oracle/达梦 共用 +CREATE SEQUENCE se_deployment_instance_seq START WITH 1 INCREMENT BY 1; +CREATE TABLE se_deployment_instance ( + id number(19) DEFAULT se_deployment_instance_seq.NEXTVAL NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + ... + PRIMARY KEY (id) +); +-- 达梦完全兼容 Oracle 语法 +``` + +### 3.5 MyBatis XML 方言适配 + +**当前问题**:XML 中硬编码 `CURRENT_TIMESTAMP`(幸运的是这个关键字在 MySQL/PG/Oracle/H2 中通用) + +**短期**:审查所有 XML,确认无数据库特定语法泄露。 + +**中期**:通过 MyBatis `` 片段 + 动态方言注入处理分页等差异。 + +### 3.6 索引策略 + +索引语法差异通过独立 `index-{db}.sql` 文件处理: +- MySQL: `ALTER TABLE ... ADD KEY ...` +- PostgreSQL: `CREATE INDEX CONCURRENTLY IF NOT EXISTS ...` +- Oracle/达梦: `CREATE INDEX ... ON ...` + +--- + +## 四、实施计划 + +### Phase 1:查询 API 增强(core 模块) + +| 任务 | 文件 | 说明 | +|------|------|------| +| 1.1 Query 接口增加条件重载 | `Query.java` | `tenantId(boolean, String)` | +| 1.2 新增 ProcessBoundQuery | `ProcessBoundQuery.java` | 共享接口 | +| 1.3 TaskQuery 继承调整 + 条件重载 | `TaskQuery.java` | 继承 ProcessBoundQuery | +| 1.4 SupervisionQuery 同上 | `SupervisionQuery.java` | | +| 1.5 NotificationQuery 同上 | `NotificationQuery.java` | | +| 1.6 AbstractProcessBoundQuery | `impl/AbstractProcessBoundQuery.java` | 公共实现 | +| 1.7 各 Impl 适配 | `TaskQueryImpl.java` 等 | 继承新基类 | +| 1.8 增加 taskStatusIn 等多值条件 | 接口 + Impl | | +| 1.9 单元测试 | `FluentQueryApiTest.java` | 补充条件重载测试 | + +### Phase 2:Oracle 系 Schema(storage-mysql 模块) + +| 任务 | 文件 | 说明 | +|------|------|------| +| 2.1 编写 Oracle/达梦 schema | `sql/schema-oracle.sql` | 基础 7 表 | +| 2.2 编写 Oracle/达梦扩展表 | `sql/workflow-enhancement-schema-oracle.sql` | 5 个扩展表 | +| 2.3 编写 Oracle/达梦索引 | `sql/index-oracle.sql` | 对应索引 | +| 2.4 编写 Oracle/达梦迁移 | `sql/migration/V001_oracle.sql` | | + +### Phase 3:Maven install + 全量测试 + +| 任务 | 说明 | +|------|------| +| 3.1 `mvn install -pl core` | core 模块安装 | +| 3.2 storage-custom 单元测试 | 验证向下兼容 | +| 3.3 storage-mysql 单元测试 | 验证 SQL 脚本 | + +--- + +## 五、向下兼容保证 + +| 变更 | 兼容性 | +|------|--------| +| Query 接口增加条件重载方法 | ✅ 纯新增,不影响现有 | +| 新增 ProcessBoundQuery 中间接口 | ✅ 子接口原有方法不变 | +| TaskQuery extends ProcessBoundQuery | ✅ 原有方法签名不变,只是继承链多了一层 | +| 新增 schema-oracle.sql | ✅ 纯新增文件 | +| AbstractQuery 不变 | ✅ 新增 AbstractProcessBoundQuery 继承它 | diff --git a/docs/design/query-api-comparison.md b/docs/design/query-api-comparison.md new file mode 100644 index 000000000..0e9a2d78c --- /dev/null +++ b/docs/design/query-api-comparison.md @@ -0,0 +1,272 @@ +# Smart-Engine vs Activiti vs Flowable: Query API 方法级对比 + +> 调研日期: 2026-02-08 (Phase 1 候选人查询完成后) +> 对比版本: Activiti 5.22.0 / 6.0, Flowable 7.x, SmartEngine dev 分支 + +--- + +## 1. 数量统计汇总 + +### TaskQuery 方法数量对比 + +| 类别 | SmartEngine | Activiti | Flowable | +|------|:-----------:|:--------:|:--------:| +| ID/主键过滤 | 3 | 4 | 7 | +| 名称/描述/文本过滤 | 4 | 9 | 9 | +| 状态过滤 | 4 | 3 | 4 | +| 用户/办理人过滤 | 3 | 7 | 11 | +| 候选人/候选组过滤 | **6** | 4 | 4 | +| 时间过滤 | 2 | 7 | 19 | +| 优先级过滤 | 1 | 3 | 3 | +| 流程定义过滤 | 6 | 14 | 15 | +| 分类/标签/扩展 | 4 | 1 | 5 | +| 变量过滤 | 0 | 22 | 35+ | +| 租户过滤 | 2 | 3 | 3 | +| 排序 | 7 | 15 | 16 | +| 分页/结果 | 6 | 4 | 4 | +| 高级特性 | 2 | 6 | 8 | +| CMMN/Scope (Flowable 独有) | 0 | 0 | 17 | +| **总计 (约)** | **~50** | **~102** | **~160+** | + +### SmartEngine 独有特性(Activiti/Flowable 都没有的) + +| 特性 | 说明 | +|------|------| +| **条件式参数 `(boolean, value)`** | 所有主要过滤方法的条件重载,避免 if-else 拼装 | +| **`taskCandidateOrGroup(userId, groupIds)`** | 单方法同时查候选人 OR 候选组 | +| **`taskTag` / `taskExtension`** | 专属标签和扩展字段过滤 | +| **`taskTitle` / `taskComment`** | 专属标题和评论字段过滤 | +| **SupervisionQuery** | 督办查询(Activiti/Flowable 无此概念) | +| **NotificationQuery** | 通知查询(Activiti/Flowable 无此概念) | +| **统一运行时+历史查询** | 无需分别调用 Runtime/Historic 两个 Service | +| **`pageOffset` / `pageSize` 链式设置** | 分页参数可预设后再 `list()` | + +--- + +## 2. TaskQuery 详细对比 + +### 2.1 ID / 主键过滤 + +| 能力 | SmartEngine | Activiti | Flowable | +|------|:-:|:-:|:-:| +| 按任务 ID | ✅ `taskInstanceId` | ✅ `taskId` | ✅ `taskId` | +| 按多个任务 ID | ❌ | ❌ | ✅ `taskIds` | +| 按执行实例 ID | ❌ | ✅ `executionId` | ✅ `executionId` | +| 按活动实例 ID | ✅ `activityInstanceId` | ❌ | ❌ | +| 按 taskDefinitionKey | ❌ | ✅ | ✅ | +| 按 taskDefinitionKey LIKE | ❌ | ✅ | ✅ | + +### 2.2 名称/描述/文本过滤 + +| 能力 | SmartEngine | Activiti | Flowable | +|------|:-:|:-:|:-:| +| 按名称 | ❌ | ✅ `taskName` | ✅ `taskName` | +| 按名称 IN | ❌ | ✅ `taskNameIn` | ✅ `taskNameIn` | +| 按名称 LIKE | ❌ | ✅ `taskNameLike` | ✅ `taskNameLike` | +| 按名称 LIKE 忽略大小写 | ❌ | ✅ | ✅ | +| 按描述 | ❌ | ✅ `taskDescription` | ✅ `taskDescription` | +| 按描述 LIKE | ❌ | ✅ `taskDescriptionLike` | ✅ `taskDescriptionLike` | +| 按标题 | ✅ `taskTitle` | ❌ | ❌ | +| 按标题 (条件式) | ✅ `taskTitle(bool, ...)` | ❌ | ❌ | +| 按评论 | ✅ `taskComment` | ❌ | ❌ | + +### 2.3 状态过滤 + +| 能力 | SmartEngine | Activiti | Flowable | +|------|:-:|:-:|:-:| +| 按状态 | ✅ `taskStatus` | ❌ | ✅ `taskState` | +| 按状态 (条件式) | ✅ `taskStatus(bool, ...)` | ❌ | ❌ | +| 按多个状态 IN | ✅ `taskStatusIn` | ❌ | ❌ | +| 仅活动 `active()` | ❌ | ✅ | ✅ | +| 仅挂起 `suspended()` | ❌ | ✅ | ✅ | + +### 2.4 用户/办理人过滤 + +| 能力 | SmartEngine | Activiti | Flowable | +|------|:-:|:-:|:-:| +| 按办理人 (assignee) | ✅ `taskAssignee` | ✅ | ✅ | +| 按办理人 (条件式) | ✅ | ❌ | ❌ | +| 按办理人 LIKE | ❌ | ✅ | ✅ | +| 按多个办理人 IDs | ❌ | ❌ | ✅ `taskAssigneeIds` | +| 未分配 `taskUnassigned()` | ❌ | ✅ | ✅ | +| 已分配 `taskAssigned()` | ❌ | ❌ | ✅ | +| 按所有者 (owner) | ❌ | ✅ | ✅ | +| 按参与用户 (involvedUser) | ❌ | ✅ | ✅ | + +### 2.5 候选人/候选组过滤 + +| 能力 | SmartEngine | Activiti | Flowable | +|------|:-:|:-:|:-:| +| 按候选人 | ✅ `taskCandidateUser` | ✅ | ✅ | +| 按候选组 | ✅ `taskCandidateGroup` | ✅ | ✅ | +| 按多个候选组 IN | ✅ `taskCandidateGroupIn` | ✅ | ✅ | +| 按多个候选组 (varargs) | ✅ | ❌ | ❌ | +| 候选人 OR 候选组 | ✅ `taskCandidateOrGroup` | ❌ | ❌ | +| 候选人 OR 已办理人 | ❌ | ✅ `taskCandidateOrAssigned` | ❌ | + +### 2.6 时间过滤 + +| 能力 | SmartEngine | Activiti | Flowable | +|------|:-:|:-:|:-:| +| 创建时间 = | ❌ | ✅ `taskCreatedOn` | ✅ | +| 创建时间 > | ❌ | ✅ `taskCreatedAfter` | ✅ | +| 创建时间 < | ❌ | ✅ `taskCreatedBefore` | ✅ | +| 完成时间 > | ✅ `completeTimeAfter` | ❌* | ❌* | +| 完成时间 < | ✅ `completeTimeBefore` | ❌* | ❌* | +| 到期日 = / > / < | ❌ | ✅ | ✅ | +| 无到期日 | ❌ | ✅ | ✅ | +| 认领时间 = / > / < | ❌ | ❌ | ✅ | + +> *Activiti/Flowable 的完成时间过滤在 HistoricTaskInstanceQuery 中,SmartEngine 合并在同一接口 + +### 2.7 流程定义过滤 + +| 能力 | SmartEngine | Activiti | Flowable | +|------|:-:|:-:|:-:| +| 按流程实例 ID | ✅ | ✅ | ✅ | +| 按流程实例 ID (条件式) | ✅ | ❌ | ❌ | +| 按多个流程实例 ID | ✅ `processInstanceIdIn` | ✅ | ✅ | +| 按流程定义 ID | ❌ | ✅ | ✅ | +| 按流程定义 Key | ❌ | ✅ | ✅ | +| 按流程定义 Key LIKE | ❌ | ✅ | ✅ | +| 按多个流程定义 Key | ❌ | ✅ | ✅ | +| 按流程定义名称 | ❌ | ✅ | ✅ | +| 按流程定义类型 | ✅ `processDefinitionType` | ❌ | ❌ | +| 按流程定义活动 ID | ✅ | ❌ | ❌ | +| 按部署 ID | ❌ | ✅ | ✅ | +| 按业务 Key | ❌ | ✅ | ✅ | + +### 2.8 变量过滤 + +| 能力 | SmartEngine | Activiti | Flowable | +|------|:-:|:-:|:-:| +| 任务变量 =, !=, >, >=, <, <=, LIKE | ❌ | ✅ (完整10+方法) | ✅ (完整12+方法) | +| 流程变量 =, !=, >, >=, <, <=, LIKE | ❌ | ✅ (完整10+方法) | ✅ (完整12+方法) | +| 变量 EXISTS / NOT EXISTS | ❌ | ❌ | ✅ | +| Case 变量 (全套) | ❌ | ❌ | ✅ | + +### 2.9 高级特性 + +| 能力 | SmartEngine | Activiti | Flowable | +|------|:-:|:-:|:-:| +| OR 查询 `or()` / `endOr()` | ❌ | ✅ | ✅ | +| 包含流程变量 | ❌ | ✅ | ✅ | +| 包含任务本地变量 | ❌ | ✅ | ✅ | +| 包含身份链接 | ❌ | ❌ | ✅ | +| 本地化 locale | ❌ | ✅ | ✅ | +| 条件式参数 (boolean guard) | ✅ | ❌ | ❌ | + +--- + +## 3. ProcessInstanceQuery 详细对比 + +### 3.1 核心过滤 + +| 能力 | SmartEngine | Activiti | Flowable | +|------|:-:|:-:|:-:| +| 按实例 ID | ✅ | ✅ | ✅ | +| 按多个实例 ID | ✅ `processInstanceIdIn` | ✅ | ✅ | +| 按状态 | ✅ `processStatus` | ❌ | ❌ | +| 仅活动 / 仅挂起 | ❌ | ✅ | ✅ | +| 按发起人 | ✅ `startedBy` | ❌ | ✅ | +| 按参与用户 | ❌ | ✅ `involvedUser` | ✅ | +| 按流程定义 Key | ❌ | ✅ | ✅ | +| 按流程定义类型 | ✅ | ❌ | ❌ | +| 按流程定义 ID+版本 | ✅ | ❌ | ❌ | +| 按父流程实例 | ✅ `parentInstanceId` | ✅ `superProcessInstanceId` | ✅ | +| 按业务唯一 ID | ✅ `bizUniqueId` | ✅ `processInstanceBusinessKey` | ✅ | +| 按业务 Key LIKE | ❌ | ❌ | ✅ | +| 启动时间 > / < | ✅ | ❌ | ✅ | +| 完成时间 > / < | ✅ | ❌ | ❌* | +| 变量过滤 (全套) | ❌ | ✅ | ✅ | +| OR 查询 | ❌ | ✅ | ✅ | +| 按子流程实例 | ❌ | ✅ | ✅ | +| 排除子流程 | ❌ | ✅ | ✅ | + +> *Flowable 的完成时间在 HistoricProcessInstanceQuery 中 + +### 3.2 Historic 查询(Activiti/Flowable 独立接口) + +SmartEngine **没有独立的 Historic 查询接口**,运行时和历史合并在同一个 Query 接口中。 + +| 能力 | SmartEngine | Activiti `HistoricProcessInstanceQuery` | Flowable | +|------|:-:|:-:|:-:| +| `finished()` | ❌ (用 `processStatus("completed")`) | ✅ | ✅ | +| `unfinished()` | ❌ (用 `processStatus("running")`) | ✅ | ✅ | +| `deleted()` / `notDeleted()` | ❌ | ✅ | ✅ | +| 完成时间范围 | ✅ `completedAfter/Before` | ✅ `finishedAfter/Before` | ✅ | +| 按持续时间排序 | ❌ | ✅ | ✅ | + +--- + +## 4. DeploymentQuery 对比 + +SmartEngine **尚无 Fluent DeploymentQuery 接口**(计划在 Phase 2 实现)。 + +| 能力 | SmartEngine | Activiti | Flowable | +|------|:-:|:-:|:-:| +| Fluent 接口 | ❌ | ✅ | ✅ | +| 按部署 ID | ❌ | ✅ | ✅ | +| 按名称 | ❌ | ✅ | ✅ | +| 按名称 LIKE | ❌ | ✅ | ✅ | +| 按分类 | ❌ | ✅ | ✅ | +| 按租户 ID | ❌ | ✅ | ✅ | +| 按流程定义 Key | ❌ | ✅ | ✅ | +| 仅最新版本 | ❌ | ❌ | ✅ `latest()` | +| 排序 | ❌ | ✅ | ✅ | + +--- + +## 5. 差距评估与优先级 + +### P1 — 高价值差距(建议尽快补齐) + +| 差距 | 说明 | 改进复杂度 | +|------|------|:---------:| +| `createdAfter` / `createdBefore` | 创建时间范围过滤,高频查询条件 | 低 | +| `taskTitleLike` | 标题模糊搜索,已有 title 字段,只需加 LIKE | 低 | +| `taskUnassigned()` | 查未认领任务,审批场景常用 | 低 | +| `processDefinitionTypeIn(List)` | 多类型过滤,CompletedTask/Process 需要 | 低 | +| DeploymentQuery | 部署查询 Fluent API,11 个过滤字段 | 中 | +| `involvedUser` | 按参与用户查流程(ProcessInstanceQuery) | 中 | + +### P2 — 中价值差距 + +| 差距 | 说明 | 改进复杂度 | +|------|------|:---------:| +| `taskAssigneeLike` | 办理人模糊搜索 | 低 | +| `taskMinPriority` / `taskMaxPriority` | 优先级范围查询 | 低 | +| `processDefinitionKey` / `processDefinitionKeyLike` | 按定义 Key 查任务 | 低 | +| `taskNameLike` / `taskDescriptionLike` | SmartEngine 用 title 代替 name | 低 | +| `taskDueDate` / `taskDueBefore` / `taskDueAfter` | 到期日(需表结构支持) | 中 | +| `withoutTenantId()` | 无租户过滤 | 低 | + +### P3 — 低优先级(暂不实现) + +| 差距 | 理由 | +|------|------| +| 变量过滤 (22+ 方法) | 需 JOIN 变量表,性能影响大,建议业务层通过 `processInstanceIdIn` 间接实现 | +| `or()` / `endOr()` | 实现复杂度高,使用场景有限 | +| `includeProcessVariables()` | 查询后单独查变量即可 | +| 本地化 `locale()` | SmartEngine 目前不涉及 i18n | +| CMMN/Scope (17 个方法) | Flowable 独有,SmartEngine 不支持 CMMN | +| `taskOwner` / `taskOwnerLike` | SmartEngine 无 owner 概念 | +| `taskDelegationState` | SmartEngine 无委派机制 | + +--- + +## 6. 总结 + +| 维度 | SmartEngine | Activiti | Flowable | +|------|:-:|:-:|:-:| +| TaskQuery 方法总数 | ~50 | ~102 | ~160+ | +| ProcessInstanceQuery 方法总数 | ~25 | ~40 | ~60+ | +| 核心业务场景覆盖度 | **85%** | 95% | 100% | +| 候选人查询 | ✅ (含独有 OR 组合) | ✅ | ✅ | +| 条件式动态查询 | ✅ (独有优势) | ❌ | ❌ | +| 变量查询 | ❌ | ✅ | ✅ | +| DeploymentQuery | ❌ (Phase 2) | ✅ | ✅ | +| 督办/通知查询 | ✅ (独有) | ❌ | ❌ | +| Historic 独立接口 | ❌ (合并设计) | ✅ | ✅ | + +**结论**:SmartEngine 在核心 BPMN 查询场景(待办、已办、候选人、流程状态)上已达到 **85% 覆盖**。主要差距集中在:创建时间过滤、模糊搜索、多值过滤等 P1 项(6 项,改进复杂度低-中),以及变量查询等 P3 项(复杂度高但使用频率低)。SmartEngine 的**条件式参数**和**督办/通知查询**是独有优势。 diff --git a/docs/design/query-api-gap-improvement-plan.md b/docs/design/query-api-gap-improvement-plan.md new file mode 100644 index 000000000..c5a9d4f4a --- /dev/null +++ b/docs/design/query-api-gap-improvement-plan.md @@ -0,0 +1,152 @@ +# Query API Gap 分析与改进计划 + +## 1. Gap 总览 + +基于与 Activiti/Flowable 的对比分析和当前未 Deprecated 旧 API 的梳理,总共识别出 **4 类 Gap**: + +| 编号 | Gap 类型 | 影响面 | 优先级 | 状态 | +|------|---------|--------|--------|------| +| G1 | TaskQuery 缺少 Assignee 候选人查询(JOIN 模式) | 4 个旧 API 无法 Deprecated | P0 | **已完成** | +| G2 | TaskQuery / ProcessInstanceQuery 缺少多值/模糊/时间范围过滤 | CompletedTask/Process 旧 API 无法 Deprecated;与 Flowable 有差距 | P1 | **已完成** | +| G3 | 缺少 DeploymentQuery Fluent API | DeploymentQueryService 无法 Deprecated | P1 | **已完成** | +| G4 | 缺少高级查询能力(or/endOr、variableValueEquals) | 与 Activiti/Flowable 有差距但业务场景不常用 | P2 | 远期规划 | + +--- + +## 2. G1:TaskQuery Assignee 候选人查询(P0)✅ 已完成 + +### 2.1 问题分析 + +当前 `TaskQuery.taskAssignee(userId)` 映射到 `claim_user_id` 字段,仅能查询**已认领**的任务。 + +但以下旧 API 使用了 `se_task_instance JOIN se_task_assignee_instance` 的模式查询**候选人待办**: + +``` +findPendingTaskList(PendingTaskQueryParam) → assigneeUserId + assigneeGroupIdList (OR 逻辑) +countPendingTaskList(PendingTaskQueryParam) → 同上 count 版本 +findTaskListByAssignee(TaskInstanceQueryByAssigneeParam) → 同上 + 支持 status 过滤 +countTaskListByAssignee(TaskInstanceQueryByAssigneeParam) → 同上 count 版本 +``` + +### 2.2 改进方案(已实现) + +```java +TaskQuery taskCandidateUser(String userId); +TaskQuery taskCandidateGroup(String groupId); +TaskQuery taskCandidateGroupIn(List groupIds); +TaskQuery taskCandidateGroupIn(String... groupIds); // default method +TaskQuery taskCandidateOrGroup(String userId, List groupIds); +``` + +**已 Deprecated**:findPendingTaskList, countPendingTaskList, findTaskListByAssignee, countTaskListByAssignee + +**测试**:23 个集成测试用例(`TaskCandidateQueryIntegrationTest`) + +--- + +## 3. G2:多值/模糊/时间范围过滤增强(P1)✅ 已完成 + +### 3.1 TaskQuery 新增方法 + +| 方法 | 用途 | 对标 Flowable | 状态 | +|------|------|--------------|------| +| `createdAfter(Date)` | 创建时间范围开始 | `taskCreatedAfter()` | ✅ | +| `createdBefore(Date)` | 创建时间范围结束 | `taskCreatedBefore()` | ✅ | +| `taskTitleLike(String)` | 标题模糊搜索 | `taskNameLike()` | ✅ | +| `taskTitleLike(boolean, String)` | 条件标题模糊搜索 | SmartEngine 特有 | ✅ | +| `taskUnassigned()` | 查询未认领任务 | `taskUnassigned()` | ✅ | +| `processDefinitionTypeIn(List)` | 多类型过滤 | `processDefinitionKeyIn()` | ✅ | +| `processDefinitionTypeIn(String...)` | 多类型过滤(varargs) | 同上 | ✅ | +| `taskAssigneeLike(String)` | 认领人模糊搜索 | `taskAssigneeLike()` | ✅ | +| `taskMinPriority(Integer)` | 最低优先级过滤 | `taskMinPriority()` | ✅ | +| `taskMaxPriority(Integer)` | 最高优先级过滤 | `taskMaxPriority()` | ✅ | + +### 3.2 ProcessInstanceQuery 新增方法 + +| 方法 | 用途 | 对标 Flowable | 状态 | +|------|------|--------------|------| +| `processDefinitionTypeIn(List)` | 多类型过滤 | `processDefinitionKeyIn()` | ✅ | +| `processDefinitionTypeIn(String...)` | 多类型过滤(varargs) | 同上 | ✅ | +| `involvedUser(String)` | 参与人过滤(当前映射为发起人) | `involvedUser()` | ✅ | + +### 3.3 所有查询通用新增方法 + +| 方法 | 用途 | 对标 Flowable | 状态 | +|------|------|--------------|------| +| `withoutTenantId()` | 跨租户查询 | `withoutTenantId()` | ✅ | + +### 3.4 MyBatis SQL 扩展 + +- `task_instance.xml` where_condition:新增 `titleLike` LIKE、`createTimeStart/End` 时间范围、`processDefinitionTypeList` IN、`unassigned` IS NULL、`claimUserIdLike` LIKE、`minPriority/maxPriority` 范围 +- `process_instance.xml` condition:新增 `processDefinitionTypeList` IN + +**测试**:38 个集成测试用例(`FluentQueryP1P2IntegrationTest`) + +--- + +## 4. G3:DeploymentQuery Fluent API(P1)✅ 已完成 + +### 4.1 实现 + +```java +public interface DeploymentQuery extends Query { + DeploymentQuery deploymentInstanceId(String id); + DeploymentQuery processDefinitionVersion(String version); + DeploymentQuery processDefinitionType(String type); + DeploymentQuery processDefinitionType(boolean condition, String type); + DeploymentQuery processDefinitionCode(String code); + DeploymentQuery processDefinitionName(String name); + DeploymentQuery processDefinitionNameLike(String nameLike); + DeploymentQuery processDefinitionNameLike(boolean condition, String nameLike); + DeploymentQuery processDefinitionDescLike(String descLike); + DeploymentQuery deploymentUserId(String userId); + DeploymentQuery deploymentStatus(String status); + DeploymentQuery deploymentStatus(boolean condition, String status); + DeploymentQuery logicStatus(String logicStatus); + DeploymentQuery orderByDeploymentId(); + DeploymentQuery orderByCreateTime(); + DeploymentQuery orderByModifyTime(); + DeploymentQuery asc(); + DeploymentQuery desc(); +} +``` + +- `SmartEngine.createDeploymentQuery()` 入口 +- `deployment_instance.xml` 添加 dynamic order_by 支持 +- **已 Deprecated**:`DeploymentQueryService` 全部 4 个方法 + +--- + +## 5. G4:高级查询能力(P2 — 远期) + +| 能力 | Activiti/Flowable | SmartEngine 计划 | +|------|-------------------|-----------------| +| `or()` / `endOr()` 组合查询 | Flowable 支持 | **暂不实现** - 使用场景有限,复杂度高 | +| `variableValueEquals(name, value)` | 两者都支持 | **暂不实现** - 需要 JOIN 变量表,性能影响大 | +| `includeProcessVariables()` | Flowable 支持 | **暂不实现** - 可在查询结果后单独查变量 | +| `taskDueDate` / `taskDueBefore` / `taskDueAfter` | 两者都支持 | **暂不实现** - 需要 schema 变更添加 due_date 列 | + +--- + +## 6. 完成后 Deprecated 进度表 + +| QueryService | 方法总数 | Phase 1 前 | G1 完成后 | G2+G3 完成后 | 保留不变 | +|---|---|---|---|---|---| +| TaskQueryService | 12 | 6 | **10** (+4) | **12** (+2) | 0 | +| ProcessQueryService | 6 | 4 | 4 | **6** (+2) | 0 | +| SupervisionQueryService | 4 | 4 | 4 | 4 | 0 | +| NotificationQueryService | 7 | 7 | 7 | 7 | 0 | +| DeploymentQueryService | 4 | 0 | 0 | **4** (+4) | 0 | +| **合计** | **33** | **21** | **25** | **33** | **0** | + +> ✅ G1+G2+G3 完成后,所有 QueryService 方法均已标记 @Deprecated,Fluent API 100% 覆盖。 + +## 7. 测试覆盖 + +| 测试类 | 测试数量 | 覆盖范围 | +|--------|---------|---------| +| TaskCandidateQueryIntegrationTest | 23 | G1: 候选人查询 | +| FluentQueryP1P2IntegrationTest | 38 | G2+G3: 全部 P1+P2 新功能 | +| **合计** | **61** | 全覆盖 | + +**全量回归**:core 91 + storage-mysql 323 + storage-custom 56 = **470 测试全部通过** diff --git a/docs/dual-storage-mode.md b/docs/dual-storage-mode.md new file mode 100644 index 000000000..d8edcb969 --- /dev/null +++ b/docs/dual-storage-mode.md @@ -0,0 +1,324 @@ +# 双存储模式共存方案 + +## 1. 背景与目标 + +SmartEngine 原有两个独立的存储模块: + +- **storage-mysql**(DATABASE 模式):基于 MyBatis 持久化到关系型数据库 +- **storage-custom**(CUSTOM 模式):基于 `PersisterSession` 的自定义存储,可用于内存、自定义持久化等场景 + +两者使用相同的 `@ExtensionBinding(group="common", bindKey=XXXStorage.class)` 注册。当两个模块同时出现在 classpath 时,`SimpleAnnotationScanner.handleDuplicatedKey()` 因 priority 相同直接抛出 `EngineException`,导致引擎初始化失败。 + +**目标**:两个 storage 模块可以同时依赖,并支持运行时通过 `StorageModeHolder`(ThreadLocal)动态切换存储模式,**服务层零改动**。 + +--- + +## 2. 架构设计 + +### 2.1 整体结构 + +``` +┌─────────────────────────────────────────────────────┐ +│ Service Layer │ +│ (DefaultProcessCommandService, DefaultTaskQuery..) │ +│ ┌──────────────────────┐ │ +│ │ cached storage ref │ ← Dynamic Proxy │ +│ └──────────┬───────────┘ │ +├────────────────────┼────────────────────────────────┤ +│ ▼ │ +│ ┌──────────────┐ │ +│ │ StorageRouter │ ← ThreadLocal 路由 │ +│ └──────┬───────┘ │ +│ ┌─────────┼─────────┐ │ +│ ▼ ▼ │ +│ ┌─────────────┐ ┌──────────────┐ │ +│ │ DATABASE │ │ CUSTOM │ │ +│ │ (MySQL impl) │ │ (Custom impl)│ │ +│ └─────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────┘ +``` + +### 2.2 三个核心机制 + +| 机制 | 解决的问题 | 关键类 | +|------|-----------|--------| +| **Group 分离** | 消除扫描冲突 | `ExtensionConstant.CUSTOM` | +| **StorageRouter** | 按模式路由到正确的存储实现 | `StorageRouter`, `StorageRegistry` | +| **动态代理** | 服务层缓存的 storage 引用能动态路由 | `SimpleAnnotationScanner.createStorageProxy()` | + +--- + +## 3. 实现细节 + +### 3.1 Phase 1: Group 分离 — 消除扫描冲突 + +**问题**:storage-mysql 和 storage-custom 都注册在 `group="common"` 下,同一 bindKey 两个实现导致冲突。 + +**方案**:新增 `ExtensionConstant.CUSTOM = "custom"` 常量,storage-custom 的 6 个 Storage 实现改为 `group="custom"`。storage-mysql 保持 `group="common"` 不变。 + +```java +// storage-custom 模块的 6 个 Storage 实现 +// Before +@ExtensionBinding(group = ExtensionConstant.COMMON, bindKey = ProcessInstanceStorage.class) + +// After +@ExtensionBinding(group = ExtensionConstant.CUSTOM, bindKey = ProcessInstanceStorage.class) +``` + +受影响的 Storage 实现: + +| 类名 | 模块 | +|------|------| +| `CustomProcessInstanceStorage` | storage-custom | +| `CustomExecutionInstanceStorage` | storage-custom | +| `CustomActivityInstanceStorage` | storage-custom | +| `CustomTaskInstanceStorage` | storage-custom | +| `CustomTaskAssigneeInstanceStorage` | storage-custom | +| `CustomVariableInstanceStorage` | storage-custom | + +### 3.2 Phase 2: StorageRouter 集成到引擎 + +**StorageRouter** 在引擎初始化时注册两组 Storage 实现,运行时按当前模式路由。 + +模式解析优先级: +1. `StorageModeHolder` ThreadLocal(请求级别覆盖) +2. `StorageContext`(上下文配置) +3. `StorageRouter.defaultMode`(默认 DATABASE) + +**引擎初始化流程**(`DefaultSmartEngine.initializeStorageRouter()`): + +``` +annotationScanner.scan() + ↓ +从 scanResult["common"] 取出实现 → 注册到 StorageRouter 的 DATABASE 模式 +从 scanResult["custom"] 取出实现 → 注册到 StorageRouter 的 CUSTOM 模式 + ↓ +如果只有一个模块存在 → 同时注册到两种模式(向后兼容) + ↓ +将 StorageRouter 设置到 Scanner(用于动态代理) +``` + +**单模块向后兼容**:当 classpath 只有 storage-custom 时,其实现会同时注册到 DATABASE 和 CUSTOM 模式下,确保默认模式(DATABASE)正常工作。 + +### 3.3 Phase 3: 动态代理 — 服务层零改动 + +**问题**:服务层(如 `DefaultExecutionQueryService`)在引擎初始化时调用 `getExtensionPoint("common", ExecutionInstanceStorage.class)` 并**缓存**返回值。此后切换 `StorageMode` 对缓存引用无效。 + +```java +// DefaultExecutionQueryService.java +public void start() { + // 初始化时调用一次,结果被缓存 + this.executionInstanceStorage = scanner.getExtensionPoint("common", ExecutionInstanceStorage.class); +} + +public List findActiveExecutionList(String id) { + // 每次调用都使用缓存的 executionInstanceStorage + return executionInstanceStorage.findActiveExecution(id, null, config); +} +``` + +**方案**:`SimpleAnnotationScanner.getExtensionPoint()` 对 StorageRouter 注册的 storage 类型返回 `java.lang.reflect.Proxy` 动态代理。代理在每次方法调用时通过 StorageRouter 根据当前 `StorageMode` 路由到正确的实现。 + +```java +// SimpleAnnotationScanner.java +public T getExtensionPoint(String group, Class clazz) { + if (storageRouter != null && "common".equals(group) && storageRouter.hasStorageType(clazz)) { + return createStorageProxy(clazz); // 返回动态代理 + } + // 非 storage 类型走原逻辑 + return (T) scanResult.get(group).getBindingMap().get(clazz); +} + +private T createStorageProxy(Class storageInterface) { + return (T) Proxy.newProxyInstance( + storageInterface.getClassLoader(), + new Class[]{storageInterface}, + (proxy, method, args) -> { + T realStorage = storageRouter.getStorage(storageInterface); // 每次动态路由 + try { + return method.invoke(realStorage, args); + } catch (InvocationTargetException e) { + throw e.getCause(); // 必须解包 InvocationTargetException + } + } + ); +} +``` + +**效果**:19 个服务类 + CommonServiceHelper **全部无需改动**。 + +--- + +## 4. 使用方式 + +### 4.1 Maven 依赖 + +单模块使用(不变): + +```xml + + + smart-engine-extension-storage-mysql + + + + + smart-engine-extension-storage-custom + +``` + +双模块共存: + +```xml + + smart-engine-extension-storage-mysql + + + smart-engine-extension-storage-custom + +``` + +### 4.2 运行时切换存储模式 + +```java +// 方式 1: ThreadLocal 请求级切换 +StorageModeHolder.set(StorageMode.CUSTOM); +try { + // 此范围内所有引擎操作使用 CUSTOM 存储 + processCommandService.start(defId, version, request); +} finally { + StorageModeHolder.clear(); +} + +// 方式 2: 设置全局默认模式 +StorageRouter router = processEngineConfiguration.getStorageRouter(); +router.setDefaultMode(StorageMode.CUSTOM); + +// 方式 3: ThreadLocal 覆盖默认模式 +router.setDefaultMode(StorageMode.CUSTOM); // 默认 CUSTOM +StorageModeHolder.set(StorageMode.DATABASE); // 此请求走 DATABASE +``` + +### 4.3 StorageMode 枚举值 + +| 枚举值 | 说明 | +|--------|------| +| `DATABASE` | 数据库存储(默认),由 storage-mysql 提供 | +| `CUSTOM` | 自定义存储(内存/自定义持久化),由 storage-custom 提供 | +| `DUAL_WRITE` | 双写模式,写入两种存储,从 CUSTOM 读 | +| `READ_MEMORY_WRITE_DATABASE` | 读 CUSTOM 写 DATABASE | + +--- + +## 5. 集成测试模块 + +新增 `extension/storage/storage-dual` 模块,同时依赖 storage-mysql 和 storage-custom,包含以下测试: + +| 测试类 | 验证内容 | +|--------|---------| +| `DualStorageEngineInitTest` | 两个模块共存时引擎正常初始化 | +| `StorageRouterRegistryTest` | DATABASE 和 CUSTOM 模式各注册了 6 种 Storage | +| `DefaultDatabaseModeTest` | 默认使用 DATABASE 模式 | +| `MemoryModeTest` | 设置默认模式为 CUSTOM | +| `ThreadLocalModeSwitchTest` | ThreadLocal 动态切换 + 覆盖默认模式 | +| `BackwardCompatibilityTest` | StorageRouter 为 null 时回退到原逻辑 | +| `DualModeProcessTest` | **端到端融合测试**:单方法内先 DATABASE 跑审批流程,再 CUSTOM 跑服务任务流程 | + +`DualModeProcessTest` 是核心验证用例,证明一个引擎实例、一个 JVM 进程内两种存储模式可以正确共存并动态切换。 + +--- + +## 6. 变更文件清单 + +### 6.1 Core 模块 — 新增文件 + +| 文件 | 说明 | +|------|------| +| `core/.../storage/StorageMode.java` | 存储模式枚举 | +| `core/.../storage/StorageRouter.java` | 存储路由器 | +| `core/.../storage/StorageRegistry.java` | 存储注册表 | +| `core/.../storage/StorageModeHolder.java` | ThreadLocal 模式持有者 | +| `core/.../storage/StorageContext.java` | 存储上下文 | +| `core/.../storage/StorageStrategy.java` | 存储策略接口 | +| `core/.../storage/strategy/DatabaseStorageStrategy.java` | DATABASE 策略 | +| `core/.../storage/strategy/CustomStorageStrategy.java` | CUSTOM 策略 | +| `core/.../storage/strategy/DualStorageStrategy.java` | 双写策略 | + +### 6.2 Core 模块 — 修改文件 + +| 文件 | 改动 | +|------|------| +| `core/.../extension/constant/ExtensionConstant.java` | 新增 `CUSTOM = "custom"` 常量 | +| `core/.../extension/scanner/SimpleAnnotationScanner.java` | 新增 `storageRouter` 字段 + `getExtensionPoint()` 返回动态代理 + `createStorageProxy()` | +| `core/.../configuration/ProcessEngineConfiguration.java` | 新增 `getStorageRouter()` / `setStorageRouter()` default 方法 | +| `core/.../configuration/impl/DefaultProcessEngineConfiguration.java` | 新增 `storageRouter` 字段 + 构造函数初始化 | +| `core/.../configuration/impl/DefaultSmartEngine.java` | 新增 `initializeStorageRouter()` 方法,在 `init()` 中调用 | + +### 6.3 storage-custom 模块 — 修改文件 + +| 文件 | 改动 | +|------|------| +| `CustomProcessInstanceStorage.java` | `group` 从 `COMMON` 改为 `CUSTOM` | +| `CustomExecutionInstanceStorage.java` | 同上 | +| `CustomActivityInstanceStorage.java` | 同上 | +| `CustomTaskInstanceStorage.java` | 同上 | +| `CustomTaskAssigneeInstanceStorage.java` | 同上 | +| `CustomVariableInstanceStorage.java` | 同上 | + +### 6.4 新增模块 + +| 路径 | 说明 | +|------|------| +| `extension/storage/storage-dual/` | 集成测试模块 | +| `extension/storage/storage-dual/pom.xml` | 依赖 core + storage-mysql + storage-custom | +| `extension/storage/storage-dual/src/test/` | 7 个测试类 + 辅助类 + BPMN + Spring 配置 | + +### 6.5 父 POM + +| 文件 | 改动 | +|------|------| +| `pom.xml`(根) | 新增 `extension/storage/storage-dual` | + +--- + +## 7. 关键设计决策与陷阱 + +### 7.1 为什么需要动态代理? + +服务类在 `start()` 生命周期中缓存 storage 引用: + +```java +this.executionInstanceStorage = scanner.getExtensionPoint("common", ExecutionInstanceStorage.class); +``` + +如果返回具体实现,切换 `StorageMode` 后缓存引用仍指向旧实现。动态代理在每次方法调用时实时路由。 + +### 7.2 循环引用陷阱 + +`StorageRouter.getStorage()` **绝不能** 回退调用 `AnnotationScanner.getExtensionPoint()`,因为 Scanner 会委托回 StorageRouter,造成 StackOverflow。所有 storage 实现在 `initializeStorageRouter()` 中预注册到 registry。 + +### 7.3 InvocationTargetException 解包 + +`java.lang.reflect.Proxy` 的 `method.invoke()` 会将异常包装为 `InvocationTargetException`,必须 `catch` 后 `throw e.getCause()` 解包,否则上层收到的异常类型不符预期。 + +### 7.4 单模块向后兼容 + +当 classpath 只有一个 storage 模块时,`initializeStorageRouter()` 将其实现注册到**两种模式**下,确保默认模式(DATABASE)总能找到实现。 + +--- + +## 8. 验证方式 + +```bash +# 单 DATABASE 模式不受影响 +mvn clean test -pl extension/storage/storage-mysql + +# 单 CUSTOM 模式不受影响 +mvn clean test -pl extension/storage/storage-custom + +# 双模式共存 + 动态切换 +mvn clean test -pl extension/storage/storage-dual + +# 全量回归 +mvn clean test +``` diff --git a/docs/jump-and-advanced.md b/docs/jump-and-advanced.md new file mode 100644 index 000000000..7799c3b18 --- /dev/null +++ b/docs/jump-and-advanced.md @@ -0,0 +1,98 @@ +# 跳转、会签、分派、VariablePersister 等高级特性 + +本章聚焦“生产系统一定会遇到,但不是入门必需”的权限: + +- jumpTo / jumpFrom:流程跳转(人工干预、补偿、迁移) +- markDone:标记完成 +- userTask 分派:候选人/候选组/组织/岗位/会签 +- VariablePersister:变量持久化策略 +- Retry:失败重试扩展 + +--- + +## 1. jumpTo / jumpFrom:流程跳转 + +入口在 `ExecutionCommandService`(签名见 `03-usage/api-guide.md`)。 + +典型用法: + +- **跳到某个节点**:线上修复,跳过失败节点 +- **从某个节点跳出**:跳过一段流程,直接进入下游 + +强烈建议: + +- 跳转前先冻结流程实例(suspend)或做并发控制 +- 跳转行为本质是“改变执行图”,请记录审计(建议写入你自己的操作日志表) +- 跳转后要处理: + - 未完成的任务如何关闭 + - 变量如何清理/补齐 + +仓库示例:`storage-custom/src/test/java/.../jump/*` + +--- + +## 2. markDone:人工标记完成 + +用于: + +- 外部系统已经完成动作,但引擎推进卡住 +- 需要人工强制结束某分支 + +注意: + +- markDone 与 jump 一样需要审计 +- DataBase 模式下要同步处理任务表/分派表状态 + +--- + +## 3. userTask 分派与会签(DataBase 模式) + +### 3.1 分派入口:TaskAssigneeDispatcher + +推荐你把所有“谁可以办这个任务”的规则收敛到一个实现类里: + +- candidateUser / candidateGroup +- org / position +- 动态规则(按变量、按角色、按业务字段) + +然后在 engine 配置中注入: + +```java +cfg.setTaskAssigneeDispatcher(new YourTaskAssigneeDispatcher()); +``` + +### 3.2 会签(multi-instance) + +仓库存在 multi-instance 相关测试资源,建议你先跑通并理解: + +- 会签是产生多个 taskInstance,还是一个 task + 多个 assignee? +- 通过条件/计数何时结束会签? + +--- + +## 4. VariablePersister:控制变量写库 + +你可以通过自定义 `VariablePersister`: + +- 控制哪些变量落库 +- 控制序列化格式(JSON/JSONB/加密) +- 控制变量清理策略(只保留最新、只保留白名单) + +--- + +## 5. Retry:失败重试 + +SmartEngine 提供: + +- `ExceptionProcessor`:统一异常入口 +- `@Retryable`:声明重试策略 +- retry 扩展模块:`extension/retry/*`(含 DB 存储实现与 schema) + +生产建议: + +- 重试要可观测(日志/指标/告警) +- 重试要可停止(熔断、最大次数) +- 对外部系统调用做幂等(避免重复扣款/重复发货) + +详见:`06-ops/failure-handling.md` + diff --git a/docs/modeling-guide.md b/docs/modeling-guide.md new file mode 100644 index 000000000..c19f72843 --- /dev/null +++ b/docs/modeling-guide.md @@ -0,0 +1,100 @@ +# 建模指南:网关 / 并行 / join / receiveTask / userTask + +SmartEngine 支持的 BPMN 子集与执行语义偏“工程落地”,建议你在建模时遵循以下规则,避免上线后出现: + +- 并行 join 卡死 +- ReceiveTask 无法推进 +- userTask 任务分派不一致 +- 变量作用域混乱 + +--- + +## 1. 基础规则(强烈建议) + +### 1.1 每个流程必须具备可追踪的唯一标识 + +- processId(BPMN process id)建议稳定、可版本化 +- 建议把“业务幂等 key / bizUniqueId”作为变量或扩展字段,贯穿 start/signal + +### 1.2 把“业务执行”放在 ServiceTask,把“等待外部事件”放在 ReceiveTask + +- ServiceTask:同步调用你的业务逻辑(委派/脚本/规则) +- ReceiveTask:等待外部 signal(回调、消息、人工确认、异步补偿) + +不要在 ServiceTask 里“自旋等待外部事件”。 + +--- + +## 2. 网关与并发 + +### 2.1 ExclusiveGateway(互斥网关) + +- 条件表达式应当是纯函数(只依赖变量) +- 避免在条件里调用外部系统(会导致不可重放、不可测试) + +### 2.2 ParallelGateway(并行网关) + +并行网关会造成: + +- execution 数量增加 +- join 需要等待全部分支到达 + +建议: + +- 分支数量可控(避免“数据驱动产生大量分支”) +- 每个分支尽量短小,避免长事务 +- 在 DataBase 模式下确保索引完备(见 `04-persistence/database-schema.md`) + +并发专题:`05-extensibility/concurrency.md` + +### 2.3 InclusiveGateway(包容网关) + +- 包容网关 join 更复杂:不是“所有分支”,而是“实际激活的分支” +- 建议你先跑仓库对应测试并理解树结构(`gateway/tree/*`) + +--- + +## 3. ReceiveTask 的正确用法 + +### 3.1 何时会进入等待状态? + +当 execution 推进到 ReceiveTask: + +- activityInstance 进入等待状态 +- executionInstance 通常处于 active 但无法继续推进 + +### 3.2 你需要怎么 signal? + +- 你必须保存 “要 signal 哪个 execution / process” 的关联信息 +- 推荐做法: + - 把 executionInstanceId 存入你的业务表(或事件 payload) + - 外部事件到达后用 `ExecutionCommandService.signal(executionInstanceId, request)` 推进 + +--- + +## 4. userTask 建模(DataBase 模式) + +userTask 的关键不在 BPMN,而在“分派规则与组织模型”: + +- candidateUser / candidateGroup / org / position 等分派语义 +- 认领/完成/转派/会签等操作语义 + +建议: + +- 把分派逻辑收敛到 `TaskAssigneeDispatcher`(统一入口) +- userTask 的业务扩展字段通过 request special key 传入(见 `RequestMapSpecialKeyConstant`) + +详见:`05-extensibility/user-integration.md` + +--- + +## 5. 流程跳转与人工干预(慎用) + +建模阶段尽量避免“依赖跳转才能跑通的流程”。跳转通常用于: + +- 线上故障补偿 +- 人工干预修复 +- 迁移/升级时的临时手段 + +详见:`03-usage/jump-and-advanced.md` + diff --git a/docs/mybatis-notes.md b/docs/mybatis-notes.md new file mode 100644 index 000000000..256ec6d5e --- /dev/null +++ b/docs/mybatis-notes.md @@ -0,0 +1,91 @@ +# MyBatis SQL 兼容、类型坑、分页/复杂查询扩展建议 + +DataBase 模式使用 MyBatis(SQLMap)实现持久化,常见问题集中在: + +- MySQL ↔ PostgreSQL 的类型差异(bigint/varchar/boolean/json/jsonb) +- 动态 SQL 的参数类型推断 +- in (...) 参数展开与 JDBC 类型 +- 分页语法(limit/offset)与复杂查询扩展 + +SQLMap 位置:`extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/` + +当前 SQLMap 文件: + +- `activity_instance.xml` +- `deployment_instance.xml` +- `execution_instance.xml` +- `notification_instance.xml` +- `process_instance.xml` +- `supervision_instance.xml` +- `task_assignee_instance.xml` +- `task_instance.xml` +- `variable_instance.xml` + +--- + +## 1. PostgreSQL 最常见报错:operator does not exist + +典型错误(你在迁移时很容易遇到): + +- `bigint = character varying` +- `smallint = boolean` + +根因: + +- MyBatis 绑定参数时按 Java 类型推断 JDBC 类型 +- PostgreSQL 对类型更严格(不会像 MySQL 那样隐式转换) + +解决建议(优先级从高到低): + +1) **保证 Java 参数类型与列类型一致**(最佳) +2) 在 XML 中显式指定 `jdbcType`(例如 BIGINT / VARCHAR / BOOLEAN) +3) 必要时在 SQL 中显式 cast(PostgreSQL `::bigint` / `CAST(... AS bigint)`) +4) 为 json/jsonb/枚举等使用自定义 `TypeHandler` + +> 你之前遇到的 `id in (?)` + `bigint = varchar`,本质也是同类问题:in 参数被当成 String 绑定了。 + +--- + +## 2. boolean / smallint 映射 + +仓库 PostgreSQL DDL 中,部分状态字段使用 `smallint`(0/1),而 Java 可能用 Boolean。 + +建议: + +- 状态字段统一使用 Integer/Short(并用枚举封装) +- 或使用 TypeHandler 显式映射(Boolean ↔ smallint) + +--- + +## 3. in (...) 参数与集合绑定 + +如果你需要 `WHERE id IN (...)`: + +- 用 MyBatis `` 展开集合 +- 确保集合元素类型与列类型一致(Long 列就传 Long 集合) + +--- + +## 4. 分页与复杂查询扩展 + +仓库的 QueryParam 多为简单字段过滤。生产常见扩展: + +- 多维度待办过滤:人员/组/组织/岗位/优先级/标签/时间区间… +- 组合条件与排序(优先级 + 更新时间 + SLA) +- 按业务字段 join(例如按订单号联查) + +建议做法: + +- 不要把所有查询都塞进引擎模块,保留“引擎最小查询集” +- 复杂查询在业务系统侧自建 read model(CQRS 的 Query 侧) + - 例如把 task_instance 与 assignee_instance join 后落到一张 denormalized 表 + - 或用 ES/ClickHouse 做全文与聚合 + +--- + +## 5. SQLMap 维护建议 + +- 为每个 SQLMap 文件建立对应的“回归测试用例” +- PostgreSQL 与 MySQL 需要“双跑”(CI 中分别跑) +- 关键 SQL(待办列表、并行网关查询)做 explain analyze 定期评估 + diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 000000000..b65c741a6 --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,110 @@ +# 扩展点总览:parser / behavior / storage / user integration / lock / executor… + +SmartEngine 的二次开发权限主要来自: + +1) **ExtensionBinding(注解扩展)**:按 group + bindKey 绑定实现 +2) **配置注入(ProcessEngineConfiguration)**:把关键组件换成你的实现 +3) **Storage 接口**:替换持久化实现(Custom/DataBase/混合) + +--- + +## 1. ExtensionBinding:如何“挂载”扩展实现 + +注解:`core/.../engine/extension/annotation/ExtensionBinding.java` + +核心字段: + +- `group`:扩展类别(见 `ExtensionConstant`) +- `bindKey`:绑定 key(通常是 BPMN 元素类型、属性名、或行为名) +- `priority`:同一 bindKey 下的优先级(越大越优先) + +扩展分组常量见:`core/.../engine/extension/constant/ExtensionConstant.java` + +- `element-parser` +- `attribute-parser` +- `activity-behavior` +- 以及 `common` / `SERVICE` + +引擎启动时会通过 `AnnotationScanner` 扫描 classpath,构建 ExtensionContainer(扩展容器)。 + +--- + +## 2. 最常用的“配置注入”扩展点 + +这些扩展点不走 ExtensionBinding,而是直接在配置里注入实现: + +- `SmartEngine getSmartEngine();` +- `void setSmartEngine(SmartEngine smartEngine);` +- `void setIdGenerator(IdGenerator idGenerator);` +- `IdGenerator getIdGenerator();` +- `void setInstanceAccessor(InstanceAccessor instanceAccessor);` +- `InstanceAccessor getInstanceAccessor();` +- `void setExpressionEvaluator(ExpressionEvaluator expressionEvaluator);` +- `ExpressionEvaluator getExpressionEvaluator();` +- `void setDelegationExecutor(DelegationExecutor delegationExecutor);` +- `DelegationExecutor getDelegationExecutor();` +- `void setListenerExecutor(ListenerExecutor listenerExecutor);` +- `ListenerExecutor getListenerExecutor();` +- `void setAnnotationScanner(AnnotationScanner annotationScanner);` +- `AnnotationScanner getAnnotationScanner();` +- `void setExceptionProcessor(ExceptionProcessor exceptionProcessor);` +- `ExceptionProcessor getExceptionProcessor();` +- `void setParallelServiceOrchestration(ParallelServiceOrchestration parallelServiceOrchestration);` +- `ParallelServiceOrchestration getParallelServiceOrchestration();` +- `void setTaskAssigneeDispatcher(TaskAssigneeDispatcher taskAssigneeDispatcher);` +- `TaskAssigneeDispatcher getTaskAssigneeDispatcher();` +- `void setVariablePersister(VariablePersister variablePersister);` +- `VariablePersister getVariablePersister();` +- `void setMultiInstanceCounter(MultiInstanceCounter multiInstanceCounter);` +- `MultiInstanceCounter getMultiInstanceCounter();` +- `void setLockStrategy(LockStrategy lockStrategy);` +- `LockStrategy getLockStrategy();` +- `void setTableSchemaStrategy(TableSchemaStrategy tableSchemaStrategy);` +- `TableSchemaStrategy getTableSchemaStrategy();` +- `void setExecutorService(ExecutorService executorService);` +- `ExecutorService getExecutorService();` +- `void setExecutorServiceMap(Map poolsMap);` +- `Map getExecutorServiceMap();` +- `void setOptionContainer(OptionContainer optionContainer);` +- `OptionContainer getOptionContainer();` +- `void setMagicExtension(Map extension);` +- `Map getMagicExtension();` +- `void setPvmActivityTaskFactory(PvmActivityTaskFactory pvmActivityTaskFactory);` +- `PvmActivityTaskFactory getPvmActivityTaskFactory();` + +> 建议阅读 `DefaultProcessEngineConfiguration`,它展示了“引擎默认怎么组装这些权限”。 + +--- + +## 3. 典型二开路径(按需求分类) + +### 3.1 你想支持更多 BPMN 扩展属性/元素 + +- 实现 ElementParser / AttributeParser +- 绑定到 `element-parser` / `attribute-parser` +- 参考:`05-extensibility/parser.md` + +### 3.2 你想改变某类节点的执行逻辑(例如 ServiceTask) + +- 实现/继承 `ActivityBehavior` +- 绑定到 `activity-behavior` +- 参考:`05-extensibility/behaviors.md` + +### 3.3 你想替换存储(换 DB、加历史表、做归档) + +- 实现 Storage 接口(`core/.../instance/storage/*`) +- 替换 DataBase 模式的 RelationshipDatabase*Storage +- 参考:`05-extensibility/storage.md` + +### 3.4 你想把 user/组织/岗位/待办融入你的 IAM/RBAC + +- 实现 `TaskAssigneeDispatcher` +- 配置 `InstanceAccessor` 与 `TaskCommandService` 的调用策略 +- 参考:`05-extensibility/user-integration.md` + +### 3.5 你想加强并行/异步执行权限 + +- 配置 `ParallelServiceOrchestration` / ExecutorService poolsMap +- 理解 `ParallelGatewayUtil` 的属性语义 +- 参考:`05-extensibility/concurrency.md` + diff --git a/docs/parser.md b/docs/parser.md new file mode 100644 index 000000000..bf72b6f72 --- /dev/null +++ b/docs/parser.md @@ -0,0 +1,62 @@ +# BPMN 解析扩展(ElementParser / AttributeParser) + +SmartEngine 在解析 BPMN 时会: + +1) 解析标准 BPMN 元素(process/startEvent/serviceTask/gateway…) +2) 解析扩展属性与扩展元素(smart:*、camunda:* 等) +3) 将结果写入 ProcessDefinition / 节点 properties,为执行阶段提供依据 + +扩展点主要有两类: + +- ElementParser:解析某类 BPMN 元素 +- AttributeParser:解析某个属性/扩展属性 + +它们都通过 `@ExtensionBinding` 挂载。 + +--- + +## 1. ElementParser + +绑定组:`ExtensionConstant.ELEMENT_PARSER` + +典型 bindKey: + +- 元素类型(serviceTask、receiveTask、parallelGateway…) +- 或你自定义的扩展元素名 + +实现建议: + +- 解析阶段只做“结构化提取”,不要做业务 side effect +- 把扩展结果写入节点 properties(或 extension map) +- 为每个 parser 写单元测试(参考 core 的 parser test) + +--- + +## 2. AttributeParser + +绑定组:`ExtensionConstant.ATTRIBUTE_PARSER` + +典型 bindKey: + +- `smart:class` +- `camunda:class` +- 以及各种 property / listener / expression + +实现建议: + +- 统一做命名空间兼容(建议复用 magicExtension 的映射) +- 明确解析优先级(priority),避免被其他实现覆盖 + +--- + +## 3. 参考用例(仓库) + +- 命名空间兼容测试:`core/src/test/java/.../ExtensionNameSpaceParseTest` +- 扩展元素解析测试:`core/src/test/resources/process-def/extend/extend.bpmn20.xml` + +如果你要支持更多 BPMN 特性(例如 boundary event、timer、event subprocess),建议: + +- 先把解析与执行行为拆开 +- parser 只负责把结构抽取出来 +- behavior 再消费 parser 产物 + diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 000000000..f8fa39384 --- /dev/null +++ b/docs/release.md @@ -0,0 +1,43 @@ +# 版本号、变更日志、兼容性与发布流程(建议) + +SmartEngine 仓库未强制规定发布流程,但如果你要在企业内“可控升级”,建议建立以下规范。 + +--- + +## 1. 版本号建议(SemVer) + +- MAJOR:破坏兼容(API/语义/表结构) +- MINOR:向后兼容新增权限 +- PATCH:向后兼容修复 + +--- + +## 2. 变更日志(Changelog) + +每次发布至少记录: + +- 新增:新增扩展点、新增行为、新增表/字段 +- 修复:修复的 bug、影响范围 +- 兼容性:是否需要重跑所有流程定义、是否需要数据迁移 + +--- + +## 3. 数据库迁移策略(DataBase 模式) + +- 所有 DDL 变更要同时提供: + - MySQL + - PostgreSQL +- 建议引入 Flyway/Liquibase 管理迁移脚本(业务系统侧做) +- 避免直接修改存量表类型(会导致迁移成本与风险飙升) + +--- + +## 4. 发布流程(企业内建议) + +1) 在 feature 分支完成开发 + 测试 +2) 合并到 dev,跑全量 CI(MySQL + PostgreSQL) +3) 打 RC(候选版),在预发环境回归 +4) 发布正式版(tag + changelog) +5) 灰度发布(按租户/按业务线) +6) 监控与回滚预案(表结构变更要提前演练) + diff --git a/docs/sharding-design.md b/docs/sharding-design.md new file mode 100644 index 000000000..350da9b0e --- /dev/null +++ b/docs/sharding-design.md @@ -0,0 +1,541 @@ +# SmartEngine 分库分表方案调研报告 + +## Context + +随着流程引擎业务量增长,11 张主表(+ 11 张归档表)的单库单表架构面临性能瓶颈。需要设计分库分表方案,逻辑上要支持按流程类型、流程 ID、任务 ID、待办任务、抄送任务等多维度高效查询。 + +**约束**: SmartEngine 需同时兼容 MySQL 和 PostgreSQL 两种数据库。 + +--- + +## 一、现状分析 + +### 1.1 数据关系全景 + +``` +ProcessInstance (根表, PK = id 即 processInstanceId) + ├── ExecutionInstance (1:N) ← process_instance_id + ├── ActivityInstance (1:N) ← process_instance_id + ├── TaskInstance (1:N) ← process_instance_id + │ ├── TaskAssigneeInstance (1:N) ← task_instance_id + process_instance_id + │ ├── SupervisionInstance (1:N) ← task_instance_id + process_instance_id + │ ├── NotificationInstance (1:N) ← task_instance_id + process_instance_id + │ ├── TaskTransferRecord (1:N) ← task_instance_id (⚠️ 缺 process_instance_id) + │ └── AssigneeOperationRecord (1:N) ← task_instance_id (⚠️ 缺 process_instance_id) + ├── VariableInstance (1:N) ← process_instance_id + └── RollbackRecord (1:N) ← process_instance_id +``` + +**关键发现**: 所有子表都已有 `process_instance_id` 列,仅 `se_task_transfer_record` 和 `se_assignee_operation_record` 缺少。 + +### 1.2 查询模式分析 + +| 查询场景 | 频率 | 查询维度 | 是否带 processInstanceId | +|---------|------|---------|------------------------| +| 用户待办列表 | ⭐⭐⭐⭐⭐ | assigneeId + status | ❌ 不带 | +| 流程任务列表 | ⭐⭐⭐⭐ | processInstanceId + status | ✅ | +| 单流程详情 | ⭐⭐⭐⭐ | processInstanceId | ✅ | +| 活跃执行查询 | ⭐⭐⭐⭐ | processInstanceId + active | ✅ | +| 任务列表查询 | ⭐⭐⭐ | processDefinitionType, domainCode 等 | 可选 | +| 抄送/通知查询 | ⭐⭐⭐ | receiverUserId + readStatus | ❌ 不带 | +| 活动历史 | ⭐⭐ | processInstanceId | ✅ | +| 督办查询 | ⭐⭐ | supervisorUserId | ❌ 不带 | + +**核心矛盾**: 80% 的写入和关联查询都围绕 `processInstanceId`,但最高频的读查询(用户待办)按 `assigneeId` 跨流程查询。 + +### 1.3 已有的分片准备 + +- `findWithShading` 方法: 查 ExecutionInstance 时同时带上 `process_instance_id` 和 `id`,说明原设计者已预见按 processInstanceId 分片 +- `StorageRouter` + 动态代理: 可插拔的存储路由,天然支持扩展新的存储策略 +- 归档机制: 已有按 `processInstanceId` 批量归档的成熟实现 + +--- + +## 二、推荐方案: 数据库原生分区 + 用户待办索引表 + +### 2.1 为什么选数据库原生分区而非中间件 + +| 维度 | 数据库原生分区 ✅ | ShardingSphere-JDBC | 应用层分库 | +|------|---------------|--------------------|---------| +| 实现复杂度 | **低** | 中 | 高 | +| 运维成本 | **低(零中间件)** | 中 | 高 | +| 代码侵入 | **极低(DDL 变更)** | 中(DataSource 替换) | 高 | +| 跨分区查询 | **数据库自动处理** | 需配置 | 手动编码 | +| 事务支持 | **原生** | XA/柔性事务 | 手动保证 | +| 扩展上限 | 单库 ~10 亿行 | 理论无限 | 理论无限 | +| 归档适配 | **现有逻辑不变** | 需适配 | 需适配 | +| StorageRouter 集成 | **无需改动** | 需新 Strategy | 需新模块 | +| MySQL/PG 兼容 | **两者均支持 HASH 分区** | 需适配方言 | 需适配方言 | + +> MySQL 5.7+ 和 PostgreSQL 10+ 都原生支持 HASH 分区。分区对应用完全透明,SQL 无需修改。对于小团队,这是**投入产出比最高**的方案。 + +### 2.2 分区键: `process_instance_id` (HASH) + +**PostgreSQL 版本:** +```sql +CREATE TABLE se_task_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + process_instance_id bigint NOT NULL, + ...其他列... + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +CREATE TABLE se_task_instance_p00 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_task_instance_p01 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 1); +... +CREATE TABLE se_task_instance_p15 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 15); +``` + +**MySQL 版本:** +```sql +CREATE TABLE se_task_instance ( + id bigint unsigned NOT NULL AUTO_INCREMENT, + process_instance_id bigint unsigned NOT NULL, + ...其他列... + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id) PARTITIONS 16; +``` + +> 两种数据库的 HASH 分区对应用层完全透明,MyBatis SQL 无需任何修改。 + +**分区数量建议**: 初始 **16 个分区**,对应单表可承载约 6000 万行。 + +**需要分区的表(共 11 张 + 对应归档表):** + +| 表 | 分区键 | 说明 | +|---|--------|------| +| `se_process_instance` | `id`(即 processInstanceId) | 根表 | +| `se_execution_instance` | `process_instance_id` | | +| `se_activity_instance` | `process_instance_id` | | +| `se_task_instance` | `process_instance_id` | | +| `se_task_assignee_instance` | `process_instance_id` | | +| `se_variable_instance` | `process_instance_id` | | +| `se_supervision_instance` | `process_instance_id` | | +| `se_notification_instance` | `process_instance_id` | | +| `se_task_transfer_record` | `process_instance_id` | ⚠️ 需新增列 | +| `se_assignee_operation_record` | `process_instance_id` | ⚠️ 需新增列 | +| `se_process_rollback_record` | `process_instance_id` | | + +**不分区的表:** +- `se_deployment_instance` — 流程定义部署表,数据量小,无需分区 +- `se_user_task_index` — 新增的用户待办索引表(见下文) +- `se_user_notification_index` — 新增的用户通知索引表 + +### 2.3 解决跨分区查询: 用户待办索引表 + +#### 问题 + +最高频查询 `findTaskByAssignee` 按 `assigneeId` 查,不带 `processInstanceId`,分区后会扫描全部 16 个分区。 + +#### 方案: 独立的 `se_user_task_index` 表 + +**PostgreSQL 版本:** +```sql +CREATE TABLE se_user_task_index ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + tenant_id varchar(64), + assignee_id varchar(255) NOT NULL, + assignee_type varchar(128) NOT NULL DEFAULT 'user', + task_instance_id bigint NOT NULL, + process_instance_id bigint NOT NULL, + process_definition_type varchar(255), + domain_code varchar(64), + extra jsonb, + task_status varchar(64) NOT NULL, + task_gmt_modified timestamp(6), + title varchar(255), + priority int DEFAULT 500, + CONSTRAINT uk_user_task_idx UNIQUE (tenant_id, assignee_id, task_instance_id) +); + +-- 部分索引(PG 特有,仅 pending 状态) +CREATE INDEX idx_user_task_pending ON se_user_task_index + (tenant_id, assignee_id, assignee_type, task_status) + WHERE task_status = 'pending'; + +CREATE INDEX idx_user_task_type ON se_user_task_index + (tenant_id, assignee_id, process_definition_type, task_status); +``` + +**MySQL 版本:** +```sql +CREATE TABLE se_user_task_index ( + id bigint unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY, + tenant_id varchar(64), + assignee_id varchar(255) NOT NULL, + assignee_type varchar(128) NOT NULL DEFAULT 'user', + task_instance_id bigint unsigned NOT NULL, + process_instance_id bigint unsigned NOT NULL, + process_definition_type varchar(255), + domain_code varchar(64), + extra json, + task_status varchar(64) NOT NULL, + task_gmt_modified datetime(6), + title varchar(255), + priority int DEFAULT 500, + UNIQUE KEY uk_user_task_idx (tenant_id, assignee_id, task_instance_id) +); + +-- MySQL 不支持部分索引,使用普通复合索引 +CREATE INDEX idx_user_task_pending ON se_user_task_index + (tenant_id, assignee_id, assignee_type, task_status); + +CREATE INDEX idx_user_task_type ON se_user_task_index + (tenant_id, assignee_id, process_definition_type, task_status); +``` + +**核心思路:** +- 不分区(只存 pending/活跃 任务,数据量可控) +- 冗余常用查询字段(processDefinitionType, domainCode, extra, title),避免 JOIN 回分区主表 +- 任务完成/取消时删除对应记录,保持表体积小 + +#### 维护时机 + +| 事件 | 操作 | +|------|------| +| TaskAssignee.insert() | INSERT 到索引表 | +| TaskInstance.update(status→completed/canceled) | DELETE 从索引表 | +| TaskInstance.update(title/priority/domainCode) | UPDATE 索引表对应字段 | +| TaskAssignee.delete() | DELETE 从索引表 | +| Archive 归档 | 索引表中已无 pending 任务,无需额外处理 | + +### 2.4 通知/抄送索引表 + +类似方案处理 `findByReceiver` 跨分区查询。结构类似 user_task_index,冗余常用字段,仅保留 unread 状态的通知。 + +**核心字段**: `receiver_user_id`, `notification_id`, `process_instance_id`, `notification_type`, `title`, `read_status`, `gmt_create` + +DDL 提供 PostgreSQL 和 MySQL 两个版本(与 user_task_index 同理)。 + +### 2.5 ID 生成策略: Snowflake 变体 + +当前默认 `AUTO_INCREMENT` 在分区表上仍可工作(PG 的 IDENTITY 列在分区表上全局递增)。但推荐迁移到 Snowflake ID: + +``` +63 22 17 0 ++--------------------+-----+-----------+ +| 41 bits timestamp | 5b | 17 bits | +| (ms since epoch) |node | sequence | ++--------------------+-----+-----------+ +``` + +- 41 bits 时间戳: ~69 年 +- 5 bits 节点 ID: 支持 32 个节点 +- 17 bits 序列号: 每 ms 131072 个 ID + +**好处**: 全局唯一、有序、可从 ID 反推节点信息、与现有 `IdGenerator` 接口兼容。 + +--- + +## 三、对各查询场景的支持分析 + +### 3.1 按流程 ID 查询 ✅ 最优 + +所有带 `processInstanceId` 的查询自动走分区裁剪(Partition Pruning),只访问 1 个分区。 + +### 3.2 按流程类型查询 ✅ 良好 + +`processDefinitionType` 不是分区键,查询会扫描所有分区。但由于每个分区内都有 `idx_tenant_id_process_definition_type` 索引,16 个分区的并行索引扫描性能仍然良好。 + +如果某个流程类型数据量极大且查询频率高,可考虑额外建立流程类型索引表(类似 user_task_index 思路)。 + +### 3.3 按任务 ID 查询 ⚠️ 需适配 + +`findOne(taskId)` 不带 `processInstanceId`,会扫描所有分区。 + +**缓解方案**: +1. 在调用链路中尽量传递 `processInstanceId`(参考现有 `findWithShading` 模式) +2. 如果使用 Snowflake ID,可从 ID 中提取分区信息 +3. 对于 `se_task_instance`,在每个分区的 `id` 列上都有 PK 索引,16 个分区并行查询 PK 索引也很快 + +### 3.4 用户待办任务 ✅ 通过索引表 + +查询 `se_user_task_index` 表(非分区、有专用索引),O(1) 效率。 + +### 3.5 抄送任务 ✅ 通过索引表 + +查询 `se_user_notification_index` 表(非分区、有专用索引)。 + +### 3.6 督办查询 ⚠️ 可接受 + +`findBySupervisor` 扫描所有分区,但督办数据量通常较小,每个分区内有索引,性能可接受。 + +--- + +## 四、需要的代码变更 + +### 4.1 DDL 变更(Schema 层) + +1. 11 张主表改为 HASH 分区表(PK 变为复合主键) +2. 11 张归档表同样改为分区表 +3. `se_task_transfer_record` 和 `se_assignee_operation_record` 新增 `process_instance_id` 列 +4. 新建 `se_user_task_index` 和 `se_user_notification_index` 表 + +### 4.2 Storage 层变更 + +- `RelationshipDatabaseTaskAssigneeInstanceStorage.insert()` — 同步维护 user_task_index +- `RelationshipDatabaseTaskInstanceStorage.update()` — status 变更时同步 user_task_index +- `findTaskByAssignee` 查询 — 改为先查 user_task_index +- `RelationshipDatabaseNotificationInstanceStorage` — 同步维护 user_notification_index +- `TaskTransferRecordStorage` / `AssigneeOperationRecordStorage` — 接口增加 processInstanceId 参数 + +### 4.3 IdGenerator 变更 + +实现 `ShardingIdGenerator`(Snowflake 变体),替换默认 ID 生成器。 + +### 4.4 归档适配 + +现有 `DefaultArchiveService` 的 `INSERT...SELECT WHERE process_instance_id IN (...)` 在分区表上自动路由,**无需修改**。仅需在归档时额外清理索引表中可能残留的记录。 + +--- + +## 五、实施路线图 + +``` +Phase 1: 基础准备 (1-2 周) + ├── 实现 ShardingIdGenerator + ├── 编写分区 DDL 脚本 + ├── se_task_transfer_record + se_assignee_operation_record 补充 process_instance_id + └── 设计并创建索引表 + +Phase 2: 索引表维护 (1-2 周) + ├── 实现 UserTaskIndexDAO + UserNotificationIndexDAO + ├── Storage 层增加索引维护逻辑 + ├── findTaskByAssignee 改为查索引表 + └── 单元测试 + +Phase 3: 数据迁移 (1 周) + ├── 编写迁移脚本(单表 → 分区表) + ├── 全量构建 se_user_task_index + ├── 测试环境验证 + └── 生产迁移(短暂停机窗口) + +Phase 4: 验证优化 (1 周) + ├── EXPLAIN ANALYZE 验证分区裁剪 + ├── 性能基准测试 + ├── 归档流程验证 + └── 全量回归测试 +``` + +--- + +## 六、未来扩展路径 + +当单库分区无法满足需求时(预计 10 亿行以上),可平滑升级: + +``` +当前: 单库 + 数据库原生 HASH 分区 (16 分区) + ↓ +中期: ShardingSphere-JDBC + 2 个库 (每库 8 分区) + ↓ +远期: ShardingSphere-JDBC + N 个库 +``` + +升级时: +- 分区键选择(`process_instance_id`)不变 +- ID 策略(Snowflake)不变 +- 索引表策略不变 +- 仅需替换 DataSource 为 ShardingSphere 配置 + +--- + +## 七、关键风险 + +| 风险 | 缓解措施 | +|------|---------| +| 分区表 PK 必须包含分区键 | 子表 PK 改为 `(id, process_instance_id)` | +| 索引表与主表瞬时不一致 | 同一事务内操作;增加定时对账任务 | +| `findOne(id)` 无分区键扫全表 | 推广 `findWithShading` 模式;从 Snowflake ID 提取分区 | +| 分区表不支持跨分区外键 | 现有表已无外键约束,不受影响 | +| 索引表数据膨胀 | 仅存 pending 状态,任务完成即删除 | +| MySQL HASH 分区与 PG 行为差异 | MySQL `PARTITION BY HASH` 用取模运算,与 PG 一致;DDL 分别维护两版本 | + +--- + +## 八、设计决策记录 + +### 8.1 分片模式开关 + +在 `OptionContainer` 中新增 `SHARDING_MODE_ENABLED`(默认 false),控制分片相关行为: + +| 场景 | sharding=false (默认) | sharding=true | +|------|------|------| +| 写入 TaskAssignee | 仅写主表 | 写主表 + 写 `se_user_task_index` | +| 写入 Notification | 仅写主表 | 写主表 + 写 `se_user_notification_index` | +| `findOne(taskId)` 不带 processInstanceId | 正常执行 | **抛异常** `ShardingKeyRequiredException` | +| `findTaskByAssignee` | 查主表 | 查 `se_user_task_index` | +| `findByReceiver` | 查主表 | 查 `se_user_notification_index` | + +**拦截细则**: +- `se_process_instance.findOne(id)` 不拦截 — PK 就是分区键,天然走分区裁剪 +- 子表的 `findOne(id)` 不带 processInstanceId 时拦截 — 会扫描全部分区 +- 拦截位置在 Storage 实现层,不污染 core 接口: + +```java +// RelationshipDatabaseTaskInstanceStorage.findOne() +if (isShardingEnabled(config) && processInstanceId == null) { + throw new ShardingKeyRequiredException( + "processInstanceId is required in sharding mode. Use findWithShading() instead."); +} +``` + +### 8.2 DDL 两套维护 + +分区模式与单表模式的 DDL 差异仅在 PK 结构和分区声明,MyBatis SQL 完全相同。 + +**文件组织:** +``` +sql/ +├── schema.sql # 单表模式(现有,不改) +├── schema-postgre.sql # PG 单表模式(现有,不改) +├── sharding/ +│ ├── schema-sharding-mysql.sql # MySQL 分区版 DDL +│ ├── schema-sharding-postgresql.sql # PG 分区版 DDL +│ ├── index-tables-mysql.sql # 索引表 (MySQL) +│ ├── index-tables-postgresql.sql # 索引表 (PG) +│ └── migration/ +│ ├── V1__add_process_instance_id.sql # 补字段 +│ └── V2__convert_to_partitioned.sql # 单表→分区迁移 +``` + +不合并成一套 DDL,因为分区表的 PK 结构不同(复合主键),强制合并会让非分片用户承担不必要开销。部署时根据 `SHARDING_MODE_ENABLED` 选用对应 DDL。 + +### 8.3 分表 vs 分库 + +当前方案的核心是**分表**(数据库原生 HASH 分区),在单库内完成。 + +| 维度 | 分表(当前方案) | 分库 | +|------|---------------|------| +| DataSource | 单个 | 多个 | +| 事务 | 原生事务 | 需 XA / 柔性事务 | +| 跨分片 JOIN | DB 自动处理 | 不可能 / 需应用层 | +| 连接数瓶颈 | 受单库限制 | 分散到多库 | +| 容量上限 | 单机磁盘 ~10 亿行 | 理论无限 | +| 实现复杂度 | DDL 变更 | 需中间件或应用层路由 | + +**对未来分库的兼容性** — 当前设计的三个决策天然兼容分库: +1. **Snowflake ID** — 全局唯一,不依赖 DB 自增,多库无冲突 +2. **processInstanceId 分片键** — 分库时同样按此键路由 +3. **Storage 层抽象** — `StorageRouter` 已支持按模式切换实现 + +当前阶段不实现分库。未来升级路径: +``` +当前: App → StorageRouter → Storage → MyBatis → 单库(16分区) +分库: App → StorageRouter → Storage → MyBatis → ShardingSphere → 多库 +``` +仅需替换基础设施层,不改业务代码。 + +### 8.4 Snowflake ID 实现注意事项 + +#### 时钟回拨保护 +``` +小回拨(≤5ms): 等待追上 +大回拨(>5ms): 抛异常拒绝生成 +``` + +#### 节点 ID 分配 +5 bits = 最多 32 个节点。默认用配置方式 `processEngineConfiguration.setNodeId()`,默认值 0。 +单节点部署不需要关心;集群部署时由运维配置或通过环境变量 `-DnodeId=N` 注入。 + +#### JavaScript 安全整数 +`Number.MAX_SAFE_INTEGER` = 2^53 - 1。Snowflake 64-bit long 可能超出。 +解决方案:JSON 序列化时 ID 转为 String(`@JsonSerialize(using = ToStringSerializer.class)` 或全局配置)。 + +#### 自定义 Epoch +不用 Unix epoch (1970),使用项目自定义 epoch 延长可用年限: +```java +// 2024-01-01 00:00:00 UTC → 41 bits 可用到 ~2093 年 +private static final long CUSTOM_EPOCH = 1704067200000L; +``` + +#### 与现有数据兼容 +- 新 Snowflake ID (~10^18) 与旧 AUTO_INCREMENT (1, 2, 3...) 天然不冲突 +- `Long` 类型不变,`IdGenerator.getIdType()` 返回 `Long.class` +- 现有数据保持原值,无需迁移 ID + +#### 线程安全 +使用 `synchronized` — 单机性能足够(每秒百万级),无锁实现复杂且收益不大。 + +--- + +## 九、使用配置指南 + +### 9.1 启用分片模式 + +在 `ProcessEngineConfiguration` 初始化时添加 `SHARDING_MODE_ENABLED_OPTION`: + +```java +ProcessEngineConfiguration config = new DefaultProcessEngineConfiguration(); +// 添加到现有 OptionContainer(不要替换,否则会丢失默认选项) +config.getOptionContainer().put(ConfigurationOption.SHARDING_MODE_ENABLED_OPTION); +``` + +> **注意**: `DefaultProcessEngineConfiguration` 构造函数初始化 `OptionContainer` 并注入 +> `EXPRESSION_COMPILE_RESULT_CACHED_OPTION` 和 `PROCESS_DEFINITION_MULTI_TENANT_SHARE_OPTION`。 +> 必须使用 `getOptionContainer().put()` 而不是 `setOptionContainer(new DefaultOptionContainer())`, +> 否则会丢失这些默认选项导致流程定义查找失败。 + +### 9.2 配置 SnowflakeIdGenerator(可选) + +SnowflakeIdGenerator 已实现但默认不启用。引擎默认使用数据库自增 ID,**在单库分区场景下完全可以正常工作**。 + +当需要全局唯一 ID(例如准备升级到多库分片)时,按如下方式启用: + +```java +ProcessEngineConfiguration config = new DefaultProcessEngineConfiguration(); + +// nodeId: 0-31,集群中每个节点唯一 +int nodeId = Integer.parseInt(System.getProperty("smartengine.nodeId", "0")); +config.setIdGenerator(new SnowflakeIdGenerator(nodeId)); +``` + +**nodeId 分配方式**: +- 单节点部署: 使用默认值 0 +- 集群部署: 通过环境变量 `-DsmartEngine.nodeId=N` 注入(0~31) +- 容器化部署: 从 `HOSTNAME` 或 Pod ordinal 自动提取 + +**数据兼容性**: 新 Snowflake ID (~10^18) 与旧 AUTO_INCREMENT (1, 2, 3...) 天然不冲突,无需迁移历史数据。 + +### 9.3 配置 Dialect(JSON 查询支持) + +若使用索引表的 JSON extra 字段查询,需配置数据库方言: + +```java +// MySQL +config.setDialect(new MySQLDialect()); + +// PostgreSQL +config.setDialect(new PostgreSQLDialect()); +``` + +### 9.4 完整配置示例 + +```java +ProcessEngineConfiguration config = new DefaultProcessEngineConfiguration(); + +// 1. 启用分片模式 +config.getOptionContainer().put(ConfigurationOption.SHARDING_MODE_ENABLED_OPTION); + +// 2. (可选) 使用 Snowflake ID +config.setIdGenerator(new SnowflakeIdGenerator(nodeId)); + +// 3. (可选) 设置数据库方言 +config.setDialect(new PostgreSQLDialect()); + +// 4. 其他配置... +config.setInstanceAccessor(yourInstanceAccessor); +config.setTaskAssigneeDispatcher(yourDispatcher); + +SmartEngine engine = new DefaultSmartEngine(); +engine.init(config); +``` + +--- + +## 十、验证方式 + +1. **分区裁剪验证**: `EXPLAIN ANALYZE SELECT * FROM se_task_instance WHERE process_instance_id = 123` — 应只扫描 1 个分区 +2. **待办查询验证**: `EXPLAIN ANALYZE SELECT * FROM se_user_task_index WHERE assignee_id = 'user1' AND task_status = 'pending'` — 应走部分索引 +3. **归档验证**: 执行归档后确认分区表数据正确迁移到归档分区表 +4. **性能对比**: JMeter 压测分区前后的 QPS 和 P99 延迟 +5. **全量回归**: SmartEngine 全部单元测试 + AuraMeta E2E 测试 diff --git a/docs/storage-overview.md b/docs/storage-overview.md new file mode 100644 index 000000000..23d336821 --- /dev/null +++ b/docs/storage-overview.md @@ -0,0 +1,94 @@ +# 存储分层:接口、扩展点、事务边界 + +SmartEngine 的存储设计核心思想是: + +- 引擎核心只依赖 **Storage 接口** +- 不同落地模式(Custom / DataBase)通过扩展模块提供实现 + +--- + +## 1. Storage 接口清单(核心) + +接口位置:`core/src/main/java/com/alibaba/smart/framework/engine/instance/storage/` + +- `ActivityInstanceStorage` +- `DeploymentInstanceStorage` +- `ExecutionHistoryInstanceStorage`(注意:仓库默认 DDL 未提供 history 表,通常由你扩展实现) +- `ExecutionInstanceStorage` +- `NotificationInstanceStorage` +- `ProcessDefinitionStorage`(流程定义存储抽象,Custom/DataBase 各自有策略) +- `ProcessInstanceStorage` +- `SupervisionInstanceStorage` +- `TaskAssigneeInstanceStorage` +- `TaskInstanceStorage` +- `VariableInstanceStorage` + +> 这些接口的入参里经常包含 `ProcessEngineConfiguration`,用于拿到 tenantId、idGenerator、instanceAccessor、variablePersister 等上下文权限。 + +--- + +## 2. DataBase 模式的实现层次 + +DataBase 模式模块:`extension/storage/storage-mysql` + +典型实现结构: + +- `persister/database/entity/*`:MyBatis entity +- `persister/database/dao/*`:DAO 接口 +- `mybatis/sqlmap/*.xml`:SQLMap +- `persister/database/service/RelationshipDatabase*Storage`:Storage 接口实现(包装 DAO,提供事务边界) + +--- + +## 3. Custom 模式的实现层次 + +Custom 模式模块:`extension/storage/storage-custom` + +它的实现基于: + +- `PersisterSession`:线程内 session(Map 存储) +- `Custom*Storage`:实现 Storage 接口 +- 单元测试用例用它来验证引擎语义(并行网关、跳转等) + +生产环境建议: + +- 以 Storage 接口为契约,实现你自己的数据库/事件日志/缓存存储 +- 明确事务边界:引擎推进与业务写入的一致性策略(强一致 or 最终一致) + +--- + +## 4. 事务边界(建议) + +### 4.1 Custom 模式(推荐强一致) + +如果你的 Storage 直接对接业务数据库: + +- 引擎推进(写 process/execution/activity/task/variable)与业务写入放同一事务 +- start/signal/complete 等外部入口做幂等(见 `03-usage/api-guide.md`) + +### 4.2 DataBase 模式(Spring Tx) + +典型做法: + +- 入口方法(Controller/Service)加 `@Transactional` +- 业务写入 + 引擎调用都在同一事务中 +- 失败回滚:避免引擎产生“孤儿实例/孤儿任务” + +--- + +## 5. 表结构策略(TableSchemaStrategy) + +引擎配置里存在 `TableSchemaStrategy`(默认 `DefaultTableSchemaStrategy`),用于: + +- 决定表名前缀、schema、或多租户策略(例如 schema-per-tenant) +- 影响 MyBatis/DAO 的 SQL 拼接(实现细节以 storage-mysql 为准) + +如果你要实现“同一套引擎同时支持 MySQL/PostgreSQL、多租户 schema 隔离”,这里是关键扩展点之一。 + +--- + +下一步: + +- DataBase 表结构与索引:`database-schema.md` +- MyBatis 兼容与类型坑:`mybatis-notes.md` + diff --git a/docs/storage.md b/docs/storage.md new file mode 100644 index 000000000..72d2a7720 --- /dev/null +++ b/docs/storage.md @@ -0,0 +1,64 @@ +# 自定义存储实现:实例/变量/任务/活动 + +如果你要把 SmartEngine 深度嵌入你的业务系统,最常见的二开需求就是: + +- 用你自己的数据库(或多租户 schema) +- 增加业务字段 +- 增加历史表/归档表 +- 做读写分离/复杂查询 + +这些都通过 Storage 接口完成。 + +--- + +## 1. Storage 接口在哪里? + +`core/.../instance/storage/*` + +每个接口对应一个“领域对象族”的持久化: + +- Process / Execution / Activity +- Task / Assignee +- Variable +- Deployment / Definition +- Notification / Supervision(增强) + +--- + +## 2. 参考实现:DataBase 模式 + +模块:`extension/storage/storage-mysql` + +实现特点: + +- RelationshipDatabase*Storage 作为 Storage 层 +- DAO + SQLMap 作为数据访问层 +- schema.sql / schema-postgre.sql 作为 DDL + +如果你要适配 PostgreSQL/MySQL 双栈: + +- 建议把“SQL 方言差异”收敛到 MyBatis provider 或专用 SQLMap +- 并为关键查询(待办、并发 join)做双库回归测试 + +--- + +## 3. 参考实现:Custom 模式 + +模块:`extension/storage/storage-custom` + +实现特点: + +- PersisterSession(线程内)做存根 +- 主要用于测试与理解接口 + + +--- + +## 4. 设计建议(生产) + +- 写入路径:尽量少的表更新、明确索引 +- 查询路径:不要依赖“引擎核心表”支撑所有复杂查询 + - 复杂待办建议做 read model(CQRS Query 侧) +- 数据保留:没有 history 表时,建议自己补齐“历史事件/操作日志” +- 多租户:建议把 tenantId 作为一级过滤条件,并建立复合索引(tenant_id + status + gmt_modified 等) + diff --git a/docs/task-domain-code-and-extra-json.md b/docs/task-domain-code-and-extra-json.md new file mode 100644 index 000000000..75fdcef8b --- /dev/null +++ b/docs/task-domain-code-and-extra-json.md @@ -0,0 +1,310 @@ +# 任务多维查询:domain_code + extra JSON 字段 + +不同流程类型的任务有各自不同的分类查询维度。原有 `extension` VARCHAR(255) 仅支持精确匹配,灵活性不足。本次新增两个字段,提供更强大的分类与结构化查询能力: + +- **`domain_code`** VARCHAR(64) — 使用方自定义语义的索引列,用于快速过滤 +- **`extra`** JSON/JSONB — 原生 JSON 类型,通过 Dialect 抽象支持跨库 JSON path 查询 + +> **向后兼容**:`extension` 字段完全不动,老客户无需迁移。 + +--- + +## 1. 使用场景 + +| 场景 | 用法 | +|------|------| +| 按业务域分类查询任务 | `domainCode("HR")` | +| 按多个业务域批量查询 | `domainCodeIn("HR", "Finance")` | +| 按 JSON 内的字段精确过滤 | `extraJson("category", "purchase")` | +| 按 JSON 内的字段多值过滤 | `extraJsonIn("priority_level", "high", "critical")` | +| 按 JSON 内的字段模糊搜索 | `extraJsonLike("department", "研发")` | +| 组合查询 | `domainCode("HR").extraJson("category", "leave")` | + +--- + +## 2. Fluent Query API + +### 2.1 domain_code 查询 + +```java +// 精确匹配 +List tasks = smartEngine.createTaskQuery() + .domainCode("HR") + .taskStatus(TaskInstanceConstant.PENDING) + .list(); + +// 条件匹配 +smartEngine.createTaskQuery() + .domainCode(StringUtils.isNotBlank(domain), domain) + .list(); + +// 多值 IN 查询 +smartEngine.createTaskQuery() + .domainCodeIn("HR", "Finance", "IT") + .list(); + +// 模糊查询 +smartEngine.createTaskQuery() + .domainCodeLike("Fin") + .list(); +``` + +### 2.2 extra JSON 查询 + +```java +// JSON key 精确匹配 +List tasks = smartEngine.createTaskQuery() + .processDefinitionType("approval") + .extraJson("category", "purchase") + .taskStatus(TaskInstanceConstant.PENDING) + .listPage(0, 10); + +// JSON key 多值 IN 查询 +smartEngine.createTaskQuery() + .extraJsonIn("priority_level", "high", "critical") + .list(); + +// JSON key 模糊查询 +smartEngine.createTaskQuery() + .extraJsonLike("department", "研发") + .list(); +``` + +### 2.3 组合查询 + +```java +// domain_code + JSON + assignee 组合 +List tasks = smartEngine.createTaskQuery() + .domainCode("HR") + .extraJson("category", "leave") + .taskCandidateUser("user001") + .taskStatus(TaskInstanceConstant.PENDING) + .orderByCreateTime().desc() + .listPage(0, 10); +``` + +### 2.4 设置字段 + +```java +// 在创建/更新任务时设置 +TaskInstance task = ...; +task.setDomainCode("HR"); +task.setExtra("{\"category\":\"leave\",\"days\":5,\"type\":\"annual\"}"); +``` + +--- + +## 3. TaskQuery 接口方法一览 + +| 方法签名 | 说明 | +|---------|------| +| `domainCode(String)` | domain_code 精确匹配 | +| `domainCode(boolean, String)` | 条件精确匹配 | +| `domainCodeIn(List)` | domain_code IN 查询 | +| `domainCodeIn(String...)` | domain_code IN 查询(varargs) | +| `domainCodeLike(String)` | domain_code 模糊查询 | +| `extraJson(String key, String value)` | JSON key 精确匹配 | +| `extraJsonIn(String key, List)` | JSON key IN 查询 | +| `extraJsonIn(String key, String...)` | JSON key IN 查询(varargs) | +| `extraJsonLike(String key, String)` | JSON key 模糊查询 | + +代码位置:`core/src/main/java/com/alibaba/smart/framework/engine/query/TaskQuery.java` + +--- + +## 4. 跨库支持(Dialect 抽象) + +### 4.1 extra 列类型 + +| 数据库 | DDL 列类型 | 说明 | +|--------|-----------|------| +| MySQL 5.7+ | `JSON` | 原生 JSON 类型,自动验证 | +| PostgreSQL 9.4+ | `JSONB` | 二进制 JSON,支持 GIN 索引 | +| Oracle 12c+ | `CLOB` + `CHECK (extra IS JSON)` | CLOB 存储 + JSON 约束 | +| H2 2.x | `CLOB` | 测试用,JSON 函数可用 | +| DM 8 (达梦) | `CLOB` | Oracle 兼容 | +| KingbaseES (人大金仓) | `JSONB` | PostgreSQL 兼容 | +| OceanBase | `JSON` | MySQL 兼容 | +| SQL Server 2016+ | `NVARCHAR(MAX)` | 内置 JSON 函数 | + +### 4.2 Dialect.jsonExtractText() 生成的 SQL + +`Dialect.jsonExtractText(column, key)` 方法根据数据库类型生成对应的 JSON 提取 SQL 表达式: + +| 数据库 | 生成的 SQL | 方言类 | +|--------|-----------|--------| +| MySQL | `JSON_UNQUOTE(JSON_EXTRACT(col, '$.key'))` | `MySqlDialect` | +| PostgreSQL | `(col->>'key')` | `PostgreSqlDialect` | +| Oracle | `JSON_VALUE(col, '$.key')` | `OracleDialect`(继承默认) | +| DM (达梦) | `JSON_VALUE(col, '$.key')` | `DmDialect`(继承默认) | +| H2 | `JSON_VALUE(col FORMAT JSON, '$.key')` | `H2Dialect` | +| KingbaseES | `(col->>'key')` | `KingbaseDialect` | +| OceanBase | `JSON_UNQUOTE(JSON_EXTRACT(col, '$.key'))` | `OceanBaseDialect` | +| SQL Server | `JSON_VALUE(col, '$.key')` | `SqlServerDialect`(继承默认) | + +代码位置:`core/src/main/java/com/alibaba/smart/framework/engine/dialect/Dialect.java` + +### 4.3 配置 Dialect + +```java +// 方式1:显式设置 +ProcessEngineConfiguration config = new DefaultProcessEngineConfiguration(); +config.setDialect(new PostgreSqlDialect()); + +// 方式2:通过 DialectRegistry 获取 +Dialect dialect = DialectRegistry.getInstance().getDialect("postgresql"); +config.setDialect(dialect); + +// 方式3:从 JDBC URL 自动检测 +Dialect dialect = DialectRegistry.getInstance().detectDialect(jdbcUrl); +config.setDialect(dialect); +``` + +> 如果未设置 Dialect,查询构建器会回退到 `DialectRegistry.getInstance().getDefaultDialect()`(默认为 MySQL)。 + +--- + +## 5. 数据库表结构变更 + +### 5.1 新增列 + +在 `se_task_instance` 表的 `extension` 列之后、`tenant_id` 列之前新增两列: + +| 列名 | MySQL | PostgreSQL | Oracle/DM | H2 | +|------|-------|-----------|-----------|-----| +| `domain_code` | `varchar(64) DEFAULT NULL` | `varchar(64) DEFAULT NULL` | `VARCHAR2(64) DEFAULT NULL` | `varchar(64) DEFAULT NULL` | +| `extra` | `json DEFAULT NULL` | `jsonb DEFAULT NULL` | `CLOB DEFAULT NULL CHECK (extra IS JSON)` | `clob DEFAULT NULL` | + +### 5.2 迁移脚本 + +已提供迁移脚本,位于 `extension/storage/storage-mysql/src/main/resources/sql/migration/`: + +**MySQL** — `V002__add_domain_code_and_extra.sql` + +```sql +ALTER TABLE se_task_instance + ADD COLUMN domain_code varchar(64) DEFAULT NULL COMMENT 'domain code', + ADD COLUMN extra json DEFAULT NULL COMMENT 'extra JSON data'; + +CREATE INDEX idx_task_domain_code ON se_task_instance(domain_code); +``` + +**PostgreSQL** — `V002__add_domain_code_and_extra_postgresql.sql` + +```sql +ALTER TABLE se_task_instance + ADD COLUMN domain_code varchar(64) DEFAULT NULL, + ADD COLUMN extra jsonb DEFAULT NULL; + +CREATE INDEX idx_task_domain_code ON se_task_instance(domain_code); +CREATE INDEX idx_task_extra_gin ON se_task_instance USING GIN (extra); +``` + +**Oracle/DM** — `V002__add_domain_code_and_extra_oracle.sql` + +```sql +ALTER TABLE se_task_instance ADD domain_code VARCHAR2(64) DEFAULT NULL; +ALTER TABLE se_task_instance ADD extra CLOB DEFAULT NULL CHECK (extra IS JSON); + +CREATE INDEX idx_task_domain_code ON se_task_instance(domain_code); +``` + +--- + +## 6. 安全性 + +### 6.1 JSON key 校验 + +所有 `extraJson*()` 方法的 key 参数经过正则校验,仅允许 `[a-zA-Z_][a-zA-Z0-9_.]*`: + +```java +private static final Pattern VALID_JSON_KEY = Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_.]*$"); +``` + +非法 key 会抛出 `IllegalArgumentException`。 + +### 6.2 SQL 注入防护 + +MyBatis XML 中使用 `${jc.sqlExpression}` 拼接 SQL 表达式,该表达式由引擎内部的 `Dialect.jsonExtractText()` 生成,不接受用户输入。JSON key 经过正则校验,值参数通过 `#{jc.value}` 参数化传递,不存在注入风险。 + +--- + +## 7. TypeHandler + +PostgreSQL 的 `JSONB` 类型需要 MyBatis TypeHandler 确保正确读写。已提供 `JsonTypeHandler`: + +代码位置:`extension/storage/storage-mysql/src/main/java/.../handler/JsonTypeHandler.java` + +该 TypeHandler 使用 `ps.setObject(i, parameter, java.sql.Types.OTHER)` 让 JDBC 驱动自动处理 JSON/JSONB 类型转换,兼容 MySQL、PostgreSQL 等数据库。 + +在 MyBatis mapper XML 中已对 `extra` 字段的 insert/update 指定了 TypeHandler: + +```xml +#{extra, typeHandler=com.alibaba.smart.framework.engine.persister.database.handler.JsonTypeHandler} +``` + +--- + +## 8. 关键文件清单 + +### Core 模块 + +| 文件 | 变更 | +|------|------| +| `dialect/Dialect.java` | +`jsonExtractText()` 接口方法 | +| `dialect/impl/AbstractDialect.java` | 默认实现(Oracle/DM/SQL Server 语法) | +| `dialect/impl/MySqlDialect.java` | MySQL JSON 函数覆盖 | +| `dialect/impl/PostgreSqlDialect.java` | PostgreSQL `->>` 操作符覆盖 | +| `dialect/impl/H2Dialect.java` | H2 `FORMAT JSON` 覆盖 | +| `dialect/impl/KingbaseDialect.java` | 同 PostgreSQL | +| `dialect/impl/OceanBaseDialect.java` | 同 MySQL | +| `configuration/ProcessEngineConfiguration.java` | +`getDialect/setDialect` | +| `configuration/impl/DefaultProcessEngineConfiguration.java` | +`dialect` 字段 | +| `model/instance/TaskInstance.java` | +`domainCode` + `extra` 接口方法 | +| `instance/impl/DefaultTaskInstance.java` | +`domainCode` + `extra` 字段 | +| `query/TaskQuery.java` | +10 个新方法 | +| `query/impl/TaskQueryImpl.java` | 完整实现 + JSON 条件构建 | +| `service/param/query/JsonCondition.java` | **新建** — JSON 精确/模糊条件模型 | +| `service/param/query/JsonInCondition.java` | **新建** — JSON IN 条件模型 | +| `service/param/query/TaskInstanceQueryParam.java` | +domainCode/jsonConditions 字段 | +| `service/param/query/TaskInstanceQueryByAssigneeParam.java` | +domainCode/jsonConditions 字段 | + +### Storage-MySQL 模块 + +| 文件 | 变更 | +|------|------| +| `entity/TaskInstanceEntity.java` | +`domainCode` + `extra` | +| `builder/TaskInstanceBuilder.java` | entity ↔ model 转换 | +| `handler/JsonTypeHandler.java` | **新建** — PostgreSQL JSONB 兼容 | +| `mybatis/sqlmap/task_instance.xml` | baseColumn/insert/update/select/where 全面更新 | +| `sql/schema.sql` | +`domain_code` + `extra` 列(MySQL) | +| `sql/schema-postgre.sql` | +`domain_code` + `extra jsonb` 列 | +| `sql/schema-oracle.sql` | +`domain_code` + `extra CLOB` 列 | +| `sql/schema-h2-only-for-test.sql` | +`domain_code` + `extra clob` 列 | +| `sql/migration/V002__*.sql` | **新建** — MySQL/PG/Oracle 迁移脚本 | + +### 测试 + +| 文件 | 测试数 | +|------|--------| +| `core/.../dialect/DialectTest.java` | +9 个 JSON 方法测试 | +| `storage-mysql/.../dao/TaskInstanceDomainCodeAndExtraTest.java` | **新建** 12 个集成测试 | + +--- + +## 9. 性能建议 + +| 建议 | 说明 | +|------|------| +| 优先使用 `domain_code` 缩小范围 | `domain_code` 有索引,先过滤再做 JSON 查询 | +| PostgreSQL 使用 GIN 索引 | 迁移脚本已包含 `CREATE INDEX ... USING GIN (extra)` | +| 避免大量 JSON 嵌套查询 | 本实现仅支持单层 key,如 `category`、`department.name` | +| Oracle CLOB 性能 | Oracle 下 `extra` 为 CLOB,JSON 查询性能较差,建议配合 `domain_code` 索引 | + +--- + +## 下一步 + +- [API 指南](api-guide.md) — 了解完整的 TaskQuery API +- [数据库表结构](database-schema.md) — 查看完整 DDL +- [Dialect 多库支持](dual-storage-mode.md) — 了解存储路由机制 diff --git a/docs/user-integration.md b/docs/user-integration.md new file mode 100644 index 000000000..8255a6f6b --- /dev/null +++ b/docs/user-integration.md @@ -0,0 +1,78 @@ +# 用户/组织/待办分派(DataBase 模式) + +DataBase 模式提供了 userTask 的待办表结构,但“谁可以办”必须与你的组织/IAM 模型对齐。 + +SmartEngine 提供两个关键扩展点: + +- InstanceAccessor:如何拿到业务系统的 bean/服务 +- TaskAssigneeDispatcher:如何从 BPMN/变量/业务规则生成 assignee 记录 + +--- + +## 1. InstanceAccessor:引擎如何拿你的业务服务 + +典型实现: + +- 在 Spring 环境中:通过 ApplicationContext.getBean(name) +- 在非 Spring 环境中:通过自建容器或反射 newInstance + +你需要保证: + +- delegation/listener/className 能映射到可用实例 +- bean 生命周期可控(单例/原型) + +--- + +## 2. TaskAssigneeDispatcher:分派的唯一入口(强烈建议) + +DataBase 模式下,userTask 的分派信息最终会落到: + +- `se_task_assignee_instance` + +Dispatcher 的职责: + +- 解析 BPMN 声明的候选规则(user/group/org/position) +- 结合流程变量与业务规则,生成最终 assignee 列表 +- 支持动态规则:例如按门店/区域/角色/值班表分派 + +仓库默认实现:`DefaultTaskAssigneeDispatcher` + +你可以做的增强: + +- 对接你的 IAM:把 role/org/position 映射成 userId 列表 +- 支持“排班/轮转”:把候选人计算交给你的排班服务 +- 支持 SLA/优先级:分派时写入 task 扩展字段 + +--- + +## 3. 待办查询与权限 + +生产里“待办查询”通常不是简单按 userId: + +- 个人:userId +- 组:groupId +- 组织:orgId +- 岗位:positionId +- 角色:roleId +- 组合:多条件 + 优先级/标签/时间 + +建议: + +- 引擎表负责“写入最小真相” +- 复杂查询在业务系统侧做 read model(例如任务视图表),避免对引擎表做复杂 join + +--- + +## 4. 任务操作语义(建议一致化) + +在你的系统侧建议统一定义操作: + +- claim(认领) +- unclaim(释放) +- complete(完成) +- transfer(转派) +- addAssignee/removeAssignee(会签/加签/减签) +- rollback(回退) + +并与增强表对齐(转派/回退/操作记录)。 + diff --git a/docs/variables.md b/docs/variables.md new file mode 100644 index 000000000..e2bec8243 --- /dev/null +++ b/docs/variables.md @@ -0,0 +1,64 @@ +# 变量与上下文:作用域、序列化、持久化策略 + +SmartEngine 的变量体系主要用于: + +- 业务数据在流程节点间流转 +- 网关条件表达式求值 +- 外部 signal 携带上下文推进流程 + +本章说明变量如何进入引擎、如何持久化、以及你需要注意的“特殊 key”。 + +--- + +## 1. 变量从哪里来? + +常见入口有三类: + +1) start 时传入 `request` map +2) signal 时传入 `request` map +3) 任务完成/认领等操作的 request map + +这些 map 会进入 `ExecutionContext`,并由 `VariableCommandService`/`VariablePersister` 决定是否持久化。 + +## 2. 变量的持久化与序列化 + +- `VariablePersister`:决定把哪些变量写入 `se_variable_instance` +- 默认实现:`DefaultVariablePersister` + +建议: + +- 只持久化“必要变量”(用于恢复推进、用于条件判断、用于审计) +- 大对象(复杂 JSON)建议存引用(如对象存储 key),避免表膨胀 +- 统一序列化策略(JSON / JSONB)并在 MyBatis typeHandler 中固定 + +## 3. 作用域(Scope) + +SmartEngine 的变量作用域通常至少区分: + +- processInstance 级(全局) +- execution/activity 级(分支/节点局部) +- task 级(userTask 表单数据) + +具体字段与实现请参考 `VariableInstance` 与 `VariableCommandService`。 + +## 4. RequestMapSpecialKeyConstant(非常重要) + +仓库定义了一组“特殊 key”,用于传递: + +- tenantId +- task 相关扩展字段(如候选人、组、组织等) +- 以及一些引擎内部控制字段 + +代码位置:`core/.../constant/RequestMapSpecialKeyConstant.java` + +建议你的系统在入口层做: + +- 白名单化:只允许特定 key 进入引擎 +- 命名空间隔离:业务变量不要使用 `_$_smart_engine_$_` 前缀 + +## 5. 常见坑 + +- **同名变量覆盖**:不同节点写同名 key,会覆盖旧值(注意作用域) +- **类型不一致**:同一 key 有时是 String、有时是 Long,会导致网关条件判断异常、或 MyBatis 写库异常 +- **PostgreSQL 类型坑**:如果 variable 表列是 jsonb/bytea,typeHandler 要严格匹配(见 `04-persistence/mybatis-notes.md`) + diff --git a/extension/archive/archive-mysql/pom.xml b/extension/archive/archive-mysql/pom.xml new file mode 100644 index 000000000..d647860d4 --- /dev/null +++ b/extension/archive/archive-mysql/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + + com.alibaba.smart.framework + smart-engine + 3.7.0-SNAPSHOT + ../../../pom.xml + + + smart-engine-extension-archive-mysql + Smart Engine Extension Archive Mysql + + + + + ${project.groupId} + smart-engine-core + ${project.version} + + + + ${project.groupId} + smart-engine-extension-storage-mysql + ${project.version} + + + + junit + junit + + + + org.slf4j + slf4j-log4j12 + ${slf4j.version} + test + + + + org.aspectj + aspectjweaver + + + aopalliance + aopalliance + + + org.aspectj + aspectjrt + + + org.springframework + spring-test + + + + org.mybatis + mybatis + + + org.mybatis + mybatis-spring + + + + mysql + mysql-connector-java + + + + diff --git a/extension/archive/archive-mysql/src/main/java/com/alibaba/smart/framework/engine/archive/config/ArchiveProperties.java b/extension/archive/archive-mysql/src/main/java/com/alibaba/smart/framework/engine/archive/config/ArchiveProperties.java new file mode 100644 index 000000000..374bfe17a --- /dev/null +++ b/extension/archive/archive-mysql/src/main/java/com/alibaba/smart/framework/engine/archive/config/ArchiveProperties.java @@ -0,0 +1,27 @@ +package com.alibaba.smart.framework.engine.archive.config; + +import lombok.Data; + +@Data +public class ArchiveProperties { + + /** + * Whether archive is enabled. Default is false. + */ + private boolean enabled = false; + + /** + * Number of days to retain completed/aborted process instances before archiving. + */ + private int retentionDays = 90; + + /** + * Number of process instances to archive per batch. + */ + private int batchSize = 100; + + /** + * Cron expression for the archive scheduler. + */ + private String cron = "0 0 2 * * ?"; +} diff --git a/extension/archive/archive-mysql/src/main/java/com/alibaba/smart/framework/engine/archive/dao/ArchiveDAO.java b/extension/archive/archive-mysql/src/main/java/com/alibaba/smart/framework/engine/archive/dao/ArchiveDAO.java new file mode 100644 index 000000000..3d8885d08 --- /dev/null +++ b/extension/archive/archive-mysql/src/main/java/com/alibaba/smart/framework/engine/archive/dao/ArchiveDAO.java @@ -0,0 +1,63 @@ +package com.alibaba.smart.framework.engine.archive.dao; + +import java.util.Date; +import java.util.List; + +import org.apache.ibatis.annotations.Param; + +public interface ArchiveDAO { + + // ========== Query archivable process instances ========== + + List findArchivableProcessIds(@Param("completedBefore") Date completedBefore, + @Param("tenantId") String tenantId, + @Param("limit") int limit); + + // ========== INSERT...SELECT archive operations ========== + + int archiveProcessInstances(@Param("ids") List ids, @Param("tenantId") String tenantId); + + int archiveTaskInstances(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int archiveExecutionInstances(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int archiveActivityInstances(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int archiveVariableInstances(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int archiveTaskAssigneeInstances(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int archiveSupervisionInstances(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int archiveNotificationInstances(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int archiveTaskTransferRecords(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int archiveAssigneeOperationRecords(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int archiveProcessRollbackRecords(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + // ========== DELETE original table data ========== + + int deleteTaskTransferRecords(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int deleteAssigneeOperationRecords(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int deleteSupervisionInstances(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int deleteNotificationInstances(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int deleteProcessRollbackRecords(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int deleteTaskAssigneeInstances(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int deleteVariableInstances(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int deleteTaskInstances(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int deleteExecutionInstances(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int deleteActivityInstances(@Param("processIds") List processIds, @Param("tenantId") String tenantId); + + int deleteProcessInstances(@Param("ids") List ids, @Param("tenantId") String tenantId); +} diff --git a/extension/archive/archive-mysql/src/main/java/com/alibaba/smart/framework/engine/archive/scheduler/ArchiveScheduler.java b/extension/archive/archive-mysql/src/main/java/com/alibaba/smart/framework/engine/archive/scheduler/ArchiveScheduler.java new file mode 100644 index 000000000..5cc780381 --- /dev/null +++ b/extension/archive/archive-mysql/src/main/java/com/alibaba/smart/framework/engine/archive/scheduler/ArchiveScheduler.java @@ -0,0 +1,57 @@ +package com.alibaba.smart.framework.engine.archive.scheduler; + +import java.util.concurrent.ScheduledFuture; + +import com.alibaba.smart.framework.engine.archive.config.ArchiveProperties; +import com.alibaba.smart.framework.engine.archive.service.ArchiveService; + +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.support.CronTrigger; + +@Slf4j +public class ArchiveScheduler { + + @Setter + private ArchiveProperties archiveProperties; + + @Setter + private ArchiveService archiveService; + + @Setter + private TaskScheduler taskScheduler; + + @Setter + private String tenantId; + + private ScheduledFuture scheduledFuture; + + public void start() { + if (!archiveProperties.isEnabled()) { + log.info("Archive scheduler is disabled"); + return; + } + + CronTrigger trigger = new CronTrigger(archiveProperties.getCron()); + scheduledFuture = taskScheduler.schedule(this::executeArchive, trigger); + log.info("Archive scheduler started with cron: {}", archiveProperties.getCron()); + } + + public void stop() { + if (scheduledFuture != null) { + scheduledFuture.cancel(false); + log.info("Archive scheduler stopped"); + } + } + + private void executeArchive() { + try { + log.info("Archive job started"); + int count = archiveService.archive(tenantId); + log.info("Archive job completed, archived {} process instances", count); + } catch (Exception e) { + log.error("Archive job failed", e); + } + } +} diff --git a/extension/archive/archive-mysql/src/main/java/com/alibaba/smart/framework/engine/archive/service/ArchiveService.java b/extension/archive/archive-mysql/src/main/java/com/alibaba/smart/framework/engine/archive/service/ArchiveService.java new file mode 100644 index 000000000..62408ad03 --- /dev/null +++ b/extension/archive/archive-mysql/src/main/java/com/alibaba/smart/framework/engine/archive/service/ArchiveService.java @@ -0,0 +1,12 @@ +package com.alibaba.smart.framework.engine.archive.service; + +public interface ArchiveService { + + /** + * Archive completed/aborted process instances for the given tenant. + * + * @param tenantId tenant id, nullable for single-tenant mode + * @return total number of archived process instances + */ + int archive(String tenantId); +} diff --git a/extension/archive/archive-mysql/src/main/java/com/alibaba/smart/framework/engine/archive/service/DefaultArchiveService.java b/extension/archive/archive-mysql/src/main/java/com/alibaba/smart/framework/engine/archive/service/DefaultArchiveService.java new file mode 100644 index 000000000..7046ed34a --- /dev/null +++ b/extension/archive/archive-mysql/src/main/java/com/alibaba/smart/framework/engine/archive/service/DefaultArchiveService.java @@ -0,0 +1,102 @@ +package com.alibaba.smart.framework.engine.archive.service; + +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import com.alibaba.smart.framework.engine.archive.config.ArchiveProperties; +import com.alibaba.smart.framework.engine.archive.dao.ArchiveDAO; + +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.support.TransactionTemplate; + +@Slf4j +public class DefaultArchiveService implements ArchiveService { + + @Setter + private ArchiveDAO archiveDAO; + + @Setter + private ArchiveProperties archiveProperties; + + @Setter + private TransactionTemplate transactionTemplate; + + @Override + public int archive(String tenantId) { + if (!archiveProperties.isEnabled()) { + log.debug("Archive is disabled, skipping"); + return 0; + } + + Date completedBefore = calculateCutoffDate(); + int totalArchived = 0; + + while (true) { + List processIds = archiveDAO.findArchivableProcessIds( + completedBefore, tenantId, archiveProperties.getBatchSize()); + + if (processIds == null || processIds.isEmpty()) { + break; + } + + int batchCount = archiveBatch(processIds, tenantId); + totalArchived += batchCount; + + log.info("Archived batch of {} process instances, total so far: {}", batchCount, totalArchived); + } + + if (totalArchived > 0) { + log.info("Archive completed. Total archived process instances: {}", totalArchived); + } + + return totalArchived; + } + + private int archiveBatch(List processIds, String tenantId) { + Integer result = transactionTemplate.execute(status -> { + try { + // Step 1: INSERT...SELECT archive all 11 tables + archiveDAO.archiveProcessInstances(processIds, tenantId); + archiveDAO.archiveTaskInstances(processIds, tenantId); + archiveDAO.archiveExecutionInstances(processIds, tenantId); + archiveDAO.archiveActivityInstances(processIds, tenantId); + archiveDAO.archiveVariableInstances(processIds, tenantId); + archiveDAO.archiveTaskAssigneeInstances(processIds, tenantId); + archiveDAO.archiveSupervisionInstances(processIds, tenantId); + archiveDAO.archiveNotificationInstances(processIds, tenantId); + archiveDAO.archiveTaskTransferRecords(processIds, tenantId); + archiveDAO.archiveAssigneeOperationRecords(processIds, tenantId); + archiveDAO.archiveProcessRollbackRecords(processIds, tenantId); + + // Step 2: DELETE from original tables (child tables first, main table last) + archiveDAO.deleteTaskTransferRecords(processIds, tenantId); + archiveDAO.deleteAssigneeOperationRecords(processIds, tenantId); + archiveDAO.deleteSupervisionInstances(processIds, tenantId); + archiveDAO.deleteNotificationInstances(processIds, tenantId); + archiveDAO.deleteProcessRollbackRecords(processIds, tenantId); + archiveDAO.deleteTaskAssigneeInstances(processIds, tenantId); + archiveDAO.deleteVariableInstances(processIds, tenantId); + archiveDAO.deleteTaskInstances(processIds, tenantId); + archiveDAO.deleteExecutionInstances(processIds, tenantId); + archiveDAO.deleteActivityInstances(processIds, tenantId); + archiveDAO.deleteProcessInstances(processIds, tenantId); + + return processIds.size(); + } catch (Exception e) { + log.error("Failed to archive batch, rolling back. processIds: {}", processIds, e); + status.setRollbackOnly(); + throw e; + } + }); + + return result != null ? result : 0; + } + + private Date calculateCutoffDate() { + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DAY_OF_MONTH, -archiveProperties.getRetentionDays()); + return cal.getTime(); + } +} diff --git a/extension/archive/archive-mysql/src/main/resources/archive-spring-example.xml b/extension/archive/archive-mysql/src/main/resources/archive-spring-example.xml new file mode 100644 index 000000000..54d8c27e0 --- /dev/null +++ b/extension/archive/archive-mysql/src/main/resources/archive-spring-example.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extension/archive/archive-mysql/src/main/resources/mybatis/sqlmap/archive.xml b/extension/archive/archive-mysql/src/main/resources/mybatis/sqlmap/archive.xml new file mode 100644 index 000000000..8dcc14860 --- /dev/null +++ b/extension/archive/archive-mysql/src/main/resources/mybatis/sqlmap/archive.xml @@ -0,0 +1,296 @@ + + + + + + + + + + + + INSERT INTO se_process_instance_archive + (id, gmt_create, gmt_modified, process_definition_id_and_version, process_definition_type, + status, complete_time, parent_process_instance_id, parent_execution_instance_id, + start_user_id, biz_unique_id, reason, comment, title, tag, tenant_id, archived_time) + SELECT id, gmt_create, gmt_modified, process_definition_id_and_version, process_definition_type, + status, complete_time, parent_process_instance_id, parent_execution_instance_id, + start_user_id, biz_unique_id, reason, comment, title, tag, tenant_id, CURRENT_TIMESTAMP(6) + FROM se_process_instance + WHERE id IN + + #{id} + + AND tenant_id = #{tenantId} + + + + INSERT INTO se_task_instance_archive + (id, gmt_create, gmt_modified, process_instance_id, process_definition_id_and_version, + process_definition_type, activity_instance_id, process_definition_activity_id, + execution_instance_id, claim_user_id, title, priority, tag, claim_time, complete_time, + status, comment, extension, domain_code, extra, tenant_id, archived_time) + SELECT id, gmt_create, gmt_modified, process_instance_id, process_definition_id_and_version, + process_definition_type, activity_instance_id, process_definition_activity_id, + execution_instance_id, claim_user_id, title, priority, tag, claim_time, complete_time, + status, comment, extension, domain_code, extra, tenant_id, CURRENT_TIMESTAMP(6) + FROM se_task_instance + WHERE process_instance_id IN + + #{id} + + AND tenant_id = #{tenantId} + + + + INSERT INTO se_execution_instance_archive + (id, gmt_create, gmt_modified, process_instance_id, process_definition_id_and_version, + process_definition_activity_id, activity_instance_id, block_id, active, tenant_id, archived_time) + SELECT id, gmt_create, gmt_modified, process_instance_id, process_definition_id_and_version, + process_definition_activity_id, activity_instance_id, block_id, active, tenant_id, CURRENT_TIMESTAMP(6) + FROM se_execution_instance + WHERE process_instance_id IN + + #{id} + + AND tenant_id = #{tenantId} + + + + INSERT INTO se_activity_instance_archive + (id, gmt_create, gmt_modified, process_instance_id, process_definition_id_and_version, + process_definition_activity_id, tenant_id, archived_time) + SELECT id, gmt_create, gmt_modified, process_instance_id, process_definition_id_and_version, + process_definition_activity_id, tenant_id, CURRENT_TIMESTAMP(6) + FROM se_activity_instance + WHERE process_instance_id IN + + #{id} + + AND tenant_id = #{tenantId} + + + + INSERT INTO se_variable_instance_archive + (id, gmt_create, gmt_modified, process_instance_id, execution_instance_id, + field_key, field_type, field_double_value, field_long_value, field_string_value, + tenant_id, archived_time) + SELECT id, gmt_create, gmt_modified, process_instance_id, execution_instance_id, + field_key, field_type, field_double_value, field_long_value, field_string_value, + tenant_id, CURRENT_TIMESTAMP(6) + FROM se_variable_instance + WHERE process_instance_id IN + + #{id} + + AND tenant_id = #{tenantId} + + + + INSERT INTO se_task_assignee_instance_archive + (id, gmt_create, gmt_modified, process_instance_id, task_instance_id, + assignee_id, assignee_type, tenant_id, archived_time) + SELECT id, gmt_create, gmt_modified, process_instance_id, task_instance_id, + assignee_id, assignee_type, tenant_id, CURRENT_TIMESTAMP(6) + FROM se_task_assignee_instance + WHERE process_instance_id IN + + #{id} + + AND tenant_id = #{tenantId} + + + + INSERT INTO se_supervision_instance_archive + (id, gmt_create, gmt_modified, process_instance_id, task_instance_id, + supervisor_user_id, supervision_reason, supervision_type, status, + close_time, tenant_id, archived_time) + SELECT id, gmt_create, gmt_modified, process_instance_id, task_instance_id, + supervisor_user_id, supervision_reason, supervision_type, status, + close_time, tenant_id, CURRENT_TIMESTAMP(6) + FROM se_supervision_instance + WHERE process_instance_id IN + + #{id} + + AND tenant_id = #{tenantId} + + + + INSERT INTO se_notification_instance_archive + (id, gmt_create, gmt_modified, process_instance_id, task_instance_id, + sender_user_id, receiver_user_id, notification_type, title, content, + read_status, read_time, tenant_id, archived_time) + SELECT id, gmt_create, gmt_modified, process_instance_id, task_instance_id, + sender_user_id, receiver_user_id, notification_type, title, content, + read_status, read_time, tenant_id, CURRENT_TIMESTAMP(6) + FROM se_notification_instance + WHERE process_instance_id IN + + #{id} + + AND tenant_id = #{tenantId} + + + + INSERT INTO se_task_transfer_record_archive + (id, gmt_create, gmt_modified, task_instance_id, process_instance_id, from_user_id, to_user_id, + transfer_reason, deadline, tenant_id, archived_time) + SELECT r.id, r.gmt_create, r.gmt_modified, r.task_instance_id, r.process_instance_id, r.from_user_id, r.to_user_id, + r.transfer_reason, r.deadline, r.tenant_id, CURRENT_TIMESTAMP(6) + FROM se_task_transfer_record r + WHERE r.task_instance_id IN ( + SELECT t.id FROM se_task_instance t WHERE t.process_instance_id IN + + #{id} + + ) + AND r.tenant_id = #{tenantId} + + + + INSERT INTO se_assignee_operation_record_archive + (id, gmt_create, gmt_modified, task_instance_id, process_instance_id, operation_type, operator_user_id, + target_user_id, operation_reason, tenant_id, archived_time) + SELECT r.id, r.gmt_create, r.gmt_modified, r.task_instance_id, r.process_instance_id, r.operation_type, r.operator_user_id, + r.target_user_id, r.operation_reason, r.tenant_id, CURRENT_TIMESTAMP(6) + FROM se_assignee_operation_record r + WHERE r.task_instance_id IN ( + SELECT t.id FROM se_task_instance t WHERE t.process_instance_id IN + + #{id} + + ) + AND r.tenant_id = #{tenantId} + + + + INSERT INTO se_process_rollback_record_archive + (id, gmt_create, gmt_modified, process_instance_id, task_instance_id, + rollback_type, from_activity_id, to_activity_id, operator_user_id, + rollback_reason, tenant_id, archived_time) + SELECT id, gmt_create, gmt_modified, process_instance_id, task_instance_id, + rollback_type, from_activity_id, to_activity_id, operator_user_id, + rollback_reason, tenant_id, CURRENT_TIMESTAMP(6) + FROM se_process_rollback_record + WHERE process_instance_id IN + + #{id} + + AND tenant_id = #{tenantId} + + + + + + DELETE FROM se_task_transfer_record + WHERE task_instance_id IN ( + SELECT id FROM se_task_instance WHERE process_instance_id IN + + #{id} + + ) + AND tenant_id = #{tenantId} + + + + DELETE FROM se_assignee_operation_record + WHERE task_instance_id IN ( + SELECT id FROM se_task_instance WHERE process_instance_id IN + + #{id} + + ) + AND tenant_id = #{tenantId} + + + + DELETE FROM se_supervision_instance + WHERE process_instance_id IN + + #{id} + + AND tenant_id = #{tenantId} + + + + DELETE FROM se_notification_instance + WHERE process_instance_id IN + + #{id} + + AND tenant_id = #{tenantId} + + + + DELETE FROM se_process_rollback_record + WHERE process_instance_id IN + + #{id} + + AND tenant_id = #{tenantId} + + + + DELETE FROM se_task_assignee_instance + WHERE process_instance_id IN + + #{id} + + AND tenant_id = #{tenantId} + + + + DELETE FROM se_variable_instance + WHERE process_instance_id IN + + #{id} + + AND tenant_id = #{tenantId} + + + + DELETE FROM se_task_instance + WHERE process_instance_id IN + + #{id} + + AND tenant_id = #{tenantId} + + + + DELETE FROM se_execution_instance + WHERE process_instance_id IN + + #{id} + + AND tenant_id = #{tenantId} + + + + DELETE FROM se_activity_instance + WHERE process_instance_id IN + + #{id} + + AND tenant_id = #{tenantId} + + + + DELETE FROM se_process_instance + WHERE id IN + + #{id} + + AND tenant_id = #{tenantId} + + + diff --git a/extension/archive/archive-mysql/src/main/resources/sql/archive-schema-mysql.sql b/extension/archive/archive-mysql/src/main/resources/sql/archive-schema-mysql.sql new file mode 100644 index 000000000..979ed6579 --- /dev/null +++ b/extension/archive/archive-mysql/src/main/resources/sql/archive-schema-mysql.sql @@ -0,0 +1,241 @@ +-- Archive tables for MySQL +-- Mirror of runtime tables with additional archived_time column +-- PK is non-auto-increment (preserves original id values) + +-- =========================================== +-- 1. se_process_instance_archive +-- =========================================== +CREATE TABLE `se_process_instance_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_definition_id_and_version` varchar(128) NOT NULL COMMENT 'process definition id and version', + `process_definition_type` varchar(255) DEFAULT NULL COMMENT 'process definition type', + `status` varchar(64) NOT NULL COMMENT '1.running 2.completed 3.aborted', + `complete_time` datetime(6) DEFAULT NULL COMMENT 'process completion time', + `parent_process_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'parent process instance id', + `parent_execution_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'parent execution instance id', + `start_user_id` varchar(128) DEFAULT NULL COMMENT 'start user id', + `biz_unique_id` varchar(255) DEFAULT NULL COMMENT 'biz unique id', + `reason` varchar(255) DEFAULT NULL COMMENT 'reason', + `comment` varchar(255) DEFAULT NULL COMMENT 'comment', + `title` varchar(255) DEFAULT NULL COMMENT 'title', + `tag` varchar(255) DEFAULT NULL COMMENT 'tag', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`), + KEY `idx_archive_pi_tenant_status` (`tenant_id`, `status`), + KEY `idx_archive_pi_archived_time` (`archived_time`) +); + +-- =========================================== +-- 2. se_task_instance_archive +-- =========================================== +CREATE TABLE `se_task_instance_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `process_definition_id_and_version` varchar(128) DEFAULT NULL COMMENT 'process definition id and version', + `process_definition_type` varchar(255) DEFAULT NULL COMMENT 'process definition type', + `activity_instance_id` bigint(20) unsigned NOT NULL COMMENT 'activity instance id', + `process_definition_activity_id` varchar(255) NOT NULL COMMENT 'process definition activity id', + `execution_instance_id` bigint(20) unsigned NOT NULL COMMENT 'execution instance id', + `claim_user_id` varchar(255) DEFAULT NULL COMMENT 'claim user id', + `title` varchar(255) DEFAULT NULL COMMENT 'title', + `priority` int(11) DEFAULT 500 COMMENT 'priority', + `tag` varchar(255) DEFAULT NULL COMMENT 'tag', + `claim_time` datetime(6) DEFAULT NULL COMMENT 'claim time', + `complete_time` datetime(6) DEFAULT NULL COMMENT 'complete time', + `status` varchar(255) NOT NULL COMMENT 'status', + `comment` varchar(255) DEFAULT NULL COMMENT 'comment', + `extension` varchar(255) DEFAULT NULL COMMENT 'extension', + `domain_code` varchar(64) DEFAULT NULL COMMENT 'domain code', + `extra` json DEFAULT NULL COMMENT 'extra JSON data', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`), + KEY `idx_archive_ti_process_instance_id` (`process_instance_id`), + KEY `idx_archive_ti_archived_time` (`archived_time`) +); + +-- =========================================== +-- 3. se_execution_instance_archive +-- =========================================== +CREATE TABLE `se_execution_instance_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `process_definition_id_and_version` varchar(255) NOT NULL COMMENT 'process definition id and version', + `process_definition_activity_id` varchar(255) NOT NULL COMMENT 'process definition activity id', + `activity_instance_id` bigint(20) unsigned NOT NULL COMMENT 'activity instance id', + `block_id` bigint(20) unsigned DEFAULT NULL COMMENT 'block_id', + `active` tinyint(4) NOT NULL COMMENT '1:active 0:inactive', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`), + KEY `idx_archive_ei_process_instance_id` (`process_instance_id`), + KEY `idx_archive_ei_archived_time` (`archived_time`) +); + +-- =========================================== +-- 4. se_activity_instance_archive +-- =========================================== +CREATE TABLE `se_activity_instance_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'process instance id', + `process_definition_id_and_version` varchar(255) NOT NULL COMMENT 'process definition id and version', + `process_definition_activity_id` varchar(64) NOT NULL COMMENT 'process definition activity id', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`), + KEY `idx_archive_ai_process_instance_id` (`process_instance_id`), + KEY `idx_archive_ai_archived_time` (`archived_time`) +); + +-- =========================================== +-- 5. se_variable_instance_archive +-- =========================================== +CREATE TABLE `se_variable_instance_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `execution_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'execution instance id', + `field_key` varchar(128) NOT NULL COMMENT 'field key', + `field_type` varchar(128) NOT NULL COMMENT 'field type', + `field_double_value` decimal(65,30) DEFAULT NULL COMMENT 'field double value', + `field_long_value` bigint(20) DEFAULT NULL COMMENT 'field long value', + `field_string_value` varchar(4000) DEFAULT NULL COMMENT 'field string value', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`), + KEY `idx_archive_vi_process_instance_id` (`process_instance_id`), + KEY `idx_archive_vi_archived_time` (`archived_time`) +); + +-- =========================================== +-- 6. se_task_assignee_instance_archive +-- =========================================== +CREATE TABLE `se_task_assignee_instance_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `assignee_id` varchar(255) NOT NULL COMMENT 'assignee id', + `assignee_type` varchar(128) NOT NULL COMMENT 'assignee type', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`), + KEY `idx_archive_tai_process_instance_id` (`process_instance_id`), + KEY `idx_archive_tai_archived_time` (`archived_time`) +); + +-- =========================================== +-- 7. se_supervision_instance_archive +-- =========================================== +CREATE TABLE `se_supervision_instance_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `supervisor_user_id` varchar(255) NOT NULL COMMENT 'supervisor user id', + `supervision_reason` varchar(500) DEFAULT NULL COMMENT 'supervision reason', + `supervision_type` varchar(64) NOT NULL COMMENT 'supervision type: urge/track/remind', + `status` varchar(64) NOT NULL COMMENT 'status: active/closed', + `close_time` datetime(6) DEFAULT NULL COMMENT 'close time', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`), + KEY `idx_archive_si_process_instance_id` (`process_instance_id`), + KEY `idx_archive_si_archived_time` (`archived_time`) +); + +-- =========================================== +-- 8. se_notification_instance_archive +-- =========================================== +CREATE TABLE `se_notification_instance_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'task instance id', + `sender_user_id` varchar(255) NOT NULL COMMENT 'sender user id', + `receiver_user_id` varchar(255) NOT NULL COMMENT 'receiver user id', + `notification_type` varchar(64) NOT NULL COMMENT 'notification type: cc/inform', + `title` varchar(255) DEFAULT NULL COMMENT 'notification title', + `content` varchar(1000) DEFAULT NULL COMMENT 'notification content', + `read_status` varchar(64) NOT NULL DEFAULT 'unread' COMMENT 'read status: unread/read', + `read_time` datetime(6) DEFAULT NULL COMMENT 'read time', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`), + KEY `idx_archive_ni_process_instance_id` (`process_instance_id`), + KEY `idx_archive_ni_archived_time` (`archived_time`) +); + +-- =========================================== +-- 9. se_task_transfer_record_archive +-- =========================================== +CREATE TABLE `se_task_transfer_record_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `process_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'process instance id', + `from_user_id` varchar(255) NOT NULL COMMENT 'from user id', + `to_user_id` varchar(255) NOT NULL COMMENT 'to user id', + `transfer_reason` varchar(500) DEFAULT NULL COMMENT 'transfer reason', + `deadline` datetime(6) DEFAULT NULL COMMENT 'processing deadline', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`), + KEY `idx_archive_ttr_task_instance_id` (`task_instance_id`), + KEY `idx_archive_ttr_archived_time` (`archived_time`) +); + +-- =========================================== +-- 10. se_assignee_operation_record_archive +-- =========================================== +CREATE TABLE `se_assignee_operation_record_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `process_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'process instance id', + `operation_type` varchar(64) NOT NULL COMMENT 'operation type: add_assignee/remove_assignee', + `operator_user_id` varchar(255) NOT NULL COMMENT 'operator user id', + `target_user_id` varchar(255) NOT NULL COMMENT 'target user id', + `operation_reason` varchar(500) DEFAULT NULL COMMENT 'operation reason', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`), + KEY `idx_archive_aor_task_instance_id` (`task_instance_id`), + KEY `idx_archive_aor_archived_time` (`archived_time`) +); + +-- =========================================== +-- 11. se_process_rollback_record_archive +-- =========================================== +CREATE TABLE `se_process_rollback_record_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `rollback_type` varchar(64) NOT NULL COMMENT 'rollback type: previous/specific', + `from_activity_id` varchar(255) NOT NULL COMMENT 'from activity id', + `to_activity_id` varchar(255) NOT NULL COMMENT 'to activity id', + `operator_user_id` varchar(255) NOT NULL COMMENT 'operator user id', + `rollback_reason` varchar(500) DEFAULT NULL COMMENT 'rollback reason', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`), + KEY `idx_archive_prr_process_instance_id` (`process_instance_id`), + KEY `idx_archive_prr_archived_time` (`archived_time`) +); diff --git a/extension/archive/archive-mysql/src/main/resources/sql/archive-schema-oracle.sql b/extension/archive/archive-mysql/src/main/resources/sql/archive-schema-oracle.sql new file mode 100644 index 000000000..44ced9ce7 --- /dev/null +++ b/extension/archive/archive-mysql/src/main/resources/sql/archive-schema-oracle.sql @@ -0,0 +1,272 @@ +-- Archive tables for Oracle/DM (DaMeng) +-- Mirror of runtime tables with additional archived_time column +-- PK is non-auto-increment (preserves original id values) + +-- =========================================== +-- 1. se_process_instance_archive +-- =========================================== +CREATE TABLE se_process_instance_archive ( + id NUMBER(19) NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_definition_id_and_version VARCHAR2(128) NOT NULL, + process_definition_type VARCHAR2(255) DEFAULT NULL, + status VARCHAR2(64) NOT NULL, + complete_time TIMESTAMP(6) DEFAULT NULL, + parent_process_instance_id NUMBER(19) DEFAULT NULL, + parent_execution_instance_id NUMBER(19) DEFAULT NULL, + start_user_id VARCHAR2(128) DEFAULT NULL, + biz_unique_id VARCHAR2(255) DEFAULT NULL, + reason VARCHAR2(255) DEFAULT NULL, + comment VARCHAR2(255) DEFAULT NULL, + title VARCHAR2(255) DEFAULT NULL, + tag VARCHAR2(255) DEFAULT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + archived_time TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL, + CONSTRAINT pk_archive_process_instance PRIMARY KEY (id) +); + +COMMENT ON TABLE se_process_instance_archive IS 'Process instance archive table'; + +CREATE INDEX idx_archive_pi_tenant_status ON se_process_instance_archive (tenant_id, status); +CREATE INDEX idx_archive_pi_archived_time ON se_process_instance_archive (archived_time); + +-- =========================================== +-- 2. se_task_instance_archive +-- =========================================== +CREATE TABLE se_task_instance_archive ( + id NUMBER(19) NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_instance_id NUMBER(19) NOT NULL, + process_definition_id_and_version VARCHAR2(128) DEFAULT NULL, + process_definition_type VARCHAR2(255) DEFAULT NULL, + activity_instance_id NUMBER(19) NOT NULL, + process_definition_activity_id VARCHAR2(255) NOT NULL, + execution_instance_id NUMBER(19) NOT NULL, + claim_user_id VARCHAR2(255) DEFAULT NULL, + title VARCHAR2(255) DEFAULT NULL, + priority NUMBER(10) DEFAULT 500, + tag VARCHAR2(255) DEFAULT NULL, + claim_time TIMESTAMP(6) DEFAULT NULL, + complete_time TIMESTAMP(6) DEFAULT NULL, + status VARCHAR2(255) NOT NULL, + comment VARCHAR2(255) DEFAULT NULL, + extension VARCHAR2(255) DEFAULT NULL, + domain_code VARCHAR2(64) DEFAULT NULL, + extra CLOB DEFAULT NULL CHECK (extra IS JSON), + tenant_id VARCHAR2(64) DEFAULT NULL, + archived_time TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL, + CONSTRAINT pk_archive_task_instance PRIMARY KEY (id) +); + +COMMENT ON TABLE se_task_instance_archive IS 'Task instance archive table'; + +CREATE INDEX idx_archive_ti_process_id ON se_task_instance_archive (process_instance_id); +CREATE INDEX idx_archive_ti_archived_time ON se_task_instance_archive (archived_time); + +-- =========================================== +-- 3. se_execution_instance_archive +-- =========================================== +CREATE TABLE se_execution_instance_archive ( + id NUMBER(19) NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_instance_id NUMBER(19) NOT NULL, + process_definition_id_and_version VARCHAR2(255) NOT NULL, + process_definition_activity_id VARCHAR2(255) NOT NULL, + activity_instance_id NUMBER(19) NOT NULL, + block_id NUMBER(19) DEFAULT NULL, + active NUMBER(1) NOT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + archived_time TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL, + CONSTRAINT pk_archive_execution_instance PRIMARY KEY (id) +); + +COMMENT ON TABLE se_execution_instance_archive IS 'Execution instance archive table'; + +CREATE INDEX idx_archive_ei_process_id ON se_execution_instance_archive (process_instance_id); +CREATE INDEX idx_archive_ei_archived_time ON se_execution_instance_archive (archived_time); + +-- =========================================== +-- 4. se_activity_instance_archive +-- =========================================== +CREATE TABLE se_activity_instance_archive ( + id NUMBER(19) NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_instance_id NUMBER(19) DEFAULT NULL, + process_definition_id_and_version VARCHAR2(255) NOT NULL, + process_definition_activity_id VARCHAR2(64) NOT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + archived_time TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL, + CONSTRAINT pk_archive_activity_instance PRIMARY KEY (id) +); + +COMMENT ON TABLE se_activity_instance_archive IS 'Activity instance archive table'; + +CREATE INDEX idx_archive_ai_process_id ON se_activity_instance_archive (process_instance_id); +CREATE INDEX idx_archive_ai_archived_time ON se_activity_instance_archive (archived_time); + +-- =========================================== +-- 5. se_variable_instance_archive +-- =========================================== +CREATE TABLE se_variable_instance_archive ( + id NUMBER(19) NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_instance_id NUMBER(19) NOT NULL, + execution_instance_id NUMBER(19) DEFAULT NULL, + field_key VARCHAR2(128) NOT NULL, + field_type VARCHAR2(128) NOT NULL, + field_double_value NUMBER(65,30) DEFAULT NULL, + field_long_value NUMBER(19) DEFAULT NULL, + field_string_value VARCHAR2(4000) DEFAULT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + archived_time TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL, + CONSTRAINT pk_archive_variable_instance PRIMARY KEY (id) +); + +COMMENT ON TABLE se_variable_instance_archive IS 'Variable instance archive table'; + +CREATE INDEX idx_archive_vi_process_id ON se_variable_instance_archive (process_instance_id); +CREATE INDEX idx_archive_vi_archived_time ON se_variable_instance_archive (archived_time); + +-- =========================================== +-- 6. se_task_assignee_instance_archive +-- =========================================== +CREATE TABLE se_task_assignee_instance_archive ( + id NUMBER(19) NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_instance_id NUMBER(19) NOT NULL, + task_instance_id NUMBER(19) NOT NULL, + assignee_id VARCHAR2(255) NOT NULL, + assignee_type VARCHAR2(128) NOT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + archived_time TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL, + CONSTRAINT pk_archive_task_assignee PRIMARY KEY (id) +); + +COMMENT ON TABLE se_task_assignee_instance_archive IS 'Task assignee instance archive table'; + +CREATE INDEX idx_archive_tai_process_id ON se_task_assignee_instance_archive (process_instance_id); +CREATE INDEX idx_archive_tai_archived_time ON se_task_assignee_instance_archive (archived_time); + +-- =========================================== +-- 7. se_supervision_instance_archive +-- =========================================== +CREATE TABLE se_supervision_instance_archive ( + id NUMBER(19) NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_instance_id NUMBER(19) NOT NULL, + task_instance_id NUMBER(19) NOT NULL, + supervisor_user_id VARCHAR2(255) NOT NULL, + supervision_reason VARCHAR2(500) DEFAULT NULL, + supervision_type VARCHAR2(64) NOT NULL, + status VARCHAR2(64) NOT NULL, + close_time TIMESTAMP(6) DEFAULT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + archived_time TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL, + CONSTRAINT pk_archive_supervision PRIMARY KEY (id) +); + +COMMENT ON TABLE se_supervision_instance_archive IS 'Supervision instance archive table'; + +CREATE INDEX idx_archive_si_process_id ON se_supervision_instance_archive (process_instance_id); +CREATE INDEX idx_archive_si_archived_time ON se_supervision_instance_archive (archived_time); + +-- =========================================== +-- 8. se_notification_instance_archive +-- =========================================== +CREATE TABLE se_notification_instance_archive ( + id NUMBER(19) NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_instance_id NUMBER(19) NOT NULL, + task_instance_id NUMBER(19) DEFAULT NULL, + sender_user_id VARCHAR2(255) NOT NULL, + receiver_user_id VARCHAR2(255) NOT NULL, + notification_type VARCHAR2(64) NOT NULL, + title VARCHAR2(255) DEFAULT NULL, + content VARCHAR2(1000) DEFAULT NULL, + read_status VARCHAR2(64) DEFAULT 'unread' NOT NULL, + read_time TIMESTAMP(6) DEFAULT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + archived_time TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL, + CONSTRAINT pk_archive_notification PRIMARY KEY (id) +); + +COMMENT ON TABLE se_notification_instance_archive IS 'Notification instance archive table'; + +CREATE INDEX idx_archive_ni_process_id ON se_notification_instance_archive (process_instance_id); +CREATE INDEX idx_archive_ni_archived_time ON se_notification_instance_archive (archived_time); + +-- =========================================== +-- 9. se_task_transfer_record_archive +-- =========================================== +CREATE TABLE se_task_transfer_record_archive ( + id NUMBER(19) NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + task_instance_id NUMBER(19) NOT NULL, + from_user_id VARCHAR2(255) NOT NULL, + to_user_id VARCHAR2(255) NOT NULL, + transfer_reason VARCHAR2(500) DEFAULT NULL, + deadline TIMESTAMP(6) DEFAULT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + archived_time TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL, + CONSTRAINT pk_archive_task_transfer PRIMARY KEY (id) +); + +COMMENT ON TABLE se_task_transfer_record_archive IS 'Task transfer record archive table'; + +CREATE INDEX idx_archive_ttr_task_id ON se_task_transfer_record_archive (task_instance_id); +CREATE INDEX idx_archive_ttr_archived_time ON se_task_transfer_record_archive (archived_time); + +-- =========================================== +-- 10. se_assignee_operation_record_archive +-- =========================================== +CREATE TABLE se_assignee_operation_record_archive ( + id NUMBER(19) NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + task_instance_id NUMBER(19) NOT NULL, + operation_type VARCHAR2(64) NOT NULL, + operator_user_id VARCHAR2(255) NOT NULL, + target_user_id VARCHAR2(255) NOT NULL, + operation_reason VARCHAR2(500) DEFAULT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + archived_time TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL, + CONSTRAINT pk_archive_assignee_op PRIMARY KEY (id) +); + +COMMENT ON TABLE se_assignee_operation_record_archive IS 'Assignee operation record archive table'; + +CREATE INDEX idx_archive_aor_task_id ON se_assignee_operation_record_archive (task_instance_id); +CREATE INDEX idx_archive_aor_archived_time ON se_assignee_operation_record_archive (archived_time); + +-- =========================================== +-- 11. se_process_rollback_record_archive +-- =========================================== +CREATE TABLE se_process_rollback_record_archive ( + id NUMBER(19) NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_instance_id NUMBER(19) NOT NULL, + task_instance_id NUMBER(19) NOT NULL, + rollback_type VARCHAR2(64) NOT NULL, + from_activity_id VARCHAR2(255) NOT NULL, + to_activity_id VARCHAR2(255) NOT NULL, + operator_user_id VARCHAR2(255) NOT NULL, + rollback_reason VARCHAR2(500) DEFAULT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + archived_time TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL, + CONSTRAINT pk_archive_rollback PRIMARY KEY (id) +); + +COMMENT ON TABLE se_process_rollback_record_archive IS 'Process rollback record archive table'; + +CREATE INDEX idx_archive_prr_process_id ON se_process_rollback_record_archive (process_instance_id); +CREATE INDEX idx_archive_prr_archived_time ON se_process_rollback_record_archive (archived_time); diff --git a/extension/archive/archive-mysql/src/main/resources/sql/archive-schema-postgre.sql b/extension/archive/archive-mysql/src/main/resources/sql/archive-schema-postgre.sql new file mode 100644 index 000000000..5db798d53 --- /dev/null +++ b/extension/archive/archive-mysql/src/main/resources/sql/archive-schema-postgre.sql @@ -0,0 +1,252 @@ +-- Archive tables for PostgreSQL +-- Mirror of runtime tables with additional archived_time column +-- PK is non-auto-increment (preserves original id values) + +-- =========================================== +-- 1. se_process_instance_archive +-- =========================================== +CREATE TABLE se_process_instance_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_definition_id_and_version varchar(128) NOT NULL, + process_definition_type varchar(255), + status varchar(64) NOT NULL, + complete_time timestamp(6), + parent_process_instance_id bigint, + parent_execution_instance_id bigint, + start_user_id varchar(128), + biz_unique_id varchar(255), + reason varchar(255), + comment varchar(255), + title varchar(255), + tag varchar(255), + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE INDEX idx_archive_pi_tenant_status ON se_process_instance_archive (tenant_id, status); +CREATE INDEX idx_archive_pi_archived_time ON se_process_instance_archive (archived_time); + +-- =========================================== +-- 2. se_task_instance_archive +-- =========================================== +CREATE TABLE se_task_instance_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + process_definition_id_and_version varchar(128), + process_definition_type varchar(255), + activity_instance_id bigint NOT NULL, + process_definition_activity_id varchar(255) NOT NULL, + execution_instance_id bigint NOT NULL, + claim_user_id varchar(255), + title varchar(255), + priority int DEFAULT 500, + tag varchar(255), + claim_time timestamp(6), + complete_time timestamp(6), + status varchar(255) NOT NULL, + comment varchar(255), + extension varchar(255), + domain_code varchar(64), + extra jsonb, + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE INDEX idx_archive_ti_process_instance_id ON se_task_instance_archive (process_instance_id); +CREATE INDEX idx_archive_ti_archived_time ON se_task_instance_archive (archived_time); + +-- =========================================== +-- 3. se_execution_instance_archive +-- =========================================== +CREATE TABLE se_execution_instance_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + process_definition_id_and_version varchar(255) NOT NULL, + process_definition_activity_id varchar(255) NOT NULL, + activity_instance_id bigint NOT NULL, + block_id bigint, + active smallint NOT NULL, + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE INDEX idx_archive_ei_process_instance_id ON se_execution_instance_archive (process_instance_id); +CREATE INDEX idx_archive_ei_archived_time ON se_execution_instance_archive (archived_time); + +-- =========================================== +-- 4. se_activity_instance_archive +-- =========================================== +CREATE TABLE se_activity_instance_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint, + process_definition_id_and_version varchar(255) NOT NULL, + process_definition_activity_id varchar(64) NOT NULL, + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE INDEX idx_archive_ai_process_instance_id ON se_activity_instance_archive (process_instance_id); +CREATE INDEX idx_archive_ai_archived_time ON se_activity_instance_archive (archived_time); + +-- =========================================== +-- 5. se_variable_instance_archive +-- =========================================== +CREATE TABLE se_variable_instance_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + execution_instance_id bigint, + field_key varchar(128) NOT NULL, + field_type varchar(128) NOT NULL, + field_double_value decimal(65,30), + field_long_value bigint, + field_string_value varchar(4000), + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE INDEX idx_archive_vi_process_instance_id ON se_variable_instance_archive (process_instance_id); +CREATE INDEX idx_archive_vi_archived_time ON se_variable_instance_archive (archived_time); + +-- =========================================== +-- 6. se_task_assignee_instance_archive +-- =========================================== +CREATE TABLE se_task_assignee_instance_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint NOT NULL, + assignee_id varchar(255) NOT NULL, + assignee_type varchar(128) NOT NULL, + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE INDEX idx_archive_tai_process_instance_id ON se_task_assignee_instance_archive (process_instance_id); +CREATE INDEX idx_archive_tai_archived_time ON se_task_assignee_instance_archive (archived_time); + +-- =========================================== +-- 7. se_supervision_instance_archive +-- =========================================== +CREATE TABLE se_supervision_instance_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint NOT NULL, + supervisor_user_id varchar(255) NOT NULL, + supervision_reason varchar(500), + supervision_type varchar(64) NOT NULL, + status varchar(64) NOT NULL, + close_time timestamp(6), + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE INDEX idx_archive_si_process_instance_id ON se_supervision_instance_archive (process_instance_id); +CREATE INDEX idx_archive_si_archived_time ON se_supervision_instance_archive (archived_time); + +-- =========================================== +-- 8. se_notification_instance_archive +-- =========================================== +CREATE TABLE se_notification_instance_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint, + sender_user_id varchar(255) NOT NULL, + receiver_user_id varchar(255) NOT NULL, + notification_type varchar(64) NOT NULL, + title varchar(255), + content varchar(1000), + read_status varchar(64) NOT NULL DEFAULT 'unread', + read_time timestamp(6), + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE INDEX idx_archive_ni_process_instance_id ON se_notification_instance_archive (process_instance_id); +CREATE INDEX idx_archive_ni_archived_time ON se_notification_instance_archive (archived_time); + +-- =========================================== +-- 9. se_task_transfer_record_archive +-- =========================================== +CREATE TABLE se_task_transfer_record_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + task_instance_id bigint NOT NULL, + process_instance_id bigint, + from_user_id varchar(255) NOT NULL, + to_user_id varchar(255) NOT NULL, + transfer_reason varchar(500), + deadline timestamp(6), + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE INDEX idx_archive_ttr_task_instance_id ON se_task_transfer_record_archive (task_instance_id); +CREATE INDEX idx_archive_ttr_archived_time ON se_task_transfer_record_archive (archived_time); + +-- =========================================== +-- 10. se_assignee_operation_record_archive +-- =========================================== +CREATE TABLE se_assignee_operation_record_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + task_instance_id bigint NOT NULL, + process_instance_id bigint, + operation_type varchar(64) NOT NULL, + operator_user_id varchar(255) NOT NULL, + target_user_id varchar(255) NOT NULL, + operation_reason varchar(500), + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE INDEX idx_archive_aor_task_instance_id ON se_assignee_operation_record_archive (task_instance_id); +CREATE INDEX idx_archive_aor_archived_time ON se_assignee_operation_record_archive (archived_time); + +-- =========================================== +-- 11. se_process_rollback_record_archive +-- =========================================== +CREATE TABLE se_process_rollback_record_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint NOT NULL, + rollback_type varchar(64) NOT NULL, + from_activity_id varchar(255) NOT NULL, + to_activity_id varchar(255) NOT NULL, + operator_user_id varchar(255) NOT NULL, + rollback_reason varchar(500), + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE INDEX idx_archive_prr_process_instance_id ON se_process_rollback_record_archive (process_instance_id); +CREATE INDEX idx_archive_prr_archived_time ON se_process_rollback_record_archive (archived_time); diff --git a/extension/archive/archive-mysql/src/main/resources/sql/migration/V3.0__create_archive_tables.sql b/extension/archive/archive-mysql/src/main/resources/sql/migration/V3.0__create_archive_tables.sql new file mode 100644 index 000000000..555c82a97 --- /dev/null +++ b/extension/archive/archive-mysql/src/main/resources/sql/migration/V3.0__create_archive_tables.sql @@ -0,0 +1,9 @@ +-- Migration V3.0: Create archive tables for MySQL +-- Purpose: Archive completed/aborted process instances to keep runtime tables lean +-- This file contains the same DDL as archive-schema-mysql.sql for Flyway/Liquibase migration + +-- Source: archive-schema-mysql.sql +-- To use this migration, copy the content from archive-schema-mysql.sql +-- or run archive-schema-mysql.sql directly on your MySQL database. + +\i archive-schema-mysql.sql diff --git a/extension/archive/archive-mysql/src/main/resources/sql/migration/V3.0__create_archive_tables_oracle.sql b/extension/archive/archive-mysql/src/main/resources/sql/migration/V3.0__create_archive_tables_oracle.sql new file mode 100644 index 000000000..cecebd244 --- /dev/null +++ b/extension/archive/archive-mysql/src/main/resources/sql/migration/V3.0__create_archive_tables_oracle.sql @@ -0,0 +1,9 @@ +-- Migration V3.0: Create archive tables for Oracle/DM +-- Purpose: Archive completed/aborted process instances to keep runtime tables lean +-- This file contains the same DDL as archive-schema-oracle.sql for Flyway/Liquibase migration + +-- Source: archive-schema-oracle.sql +-- To use this migration, copy the content from archive-schema-oracle.sql +-- or run archive-schema-oracle.sql directly on your Oracle/DM database. + +@archive-schema-oracle.sql diff --git a/extension/archive/archive-mysql/src/main/resources/sql/migration/V3.0__create_archive_tables_postgresql.sql b/extension/archive/archive-mysql/src/main/resources/sql/migration/V3.0__create_archive_tables_postgresql.sql new file mode 100644 index 000000000..1f25714eb --- /dev/null +++ b/extension/archive/archive-mysql/src/main/resources/sql/migration/V3.0__create_archive_tables_postgresql.sql @@ -0,0 +1,9 @@ +-- Migration V3.0: Create archive tables for PostgreSQL +-- Purpose: Archive completed/aborted process instances to keep runtime tables lean +-- This file contains the same DDL as archive-schema-postgre.sql for Flyway/Liquibase migration + +-- Source: archive-schema-postgre.sql +-- To use this migration, copy the content from archive-schema-postgre.sql +-- or run archive-schema-postgre.sql directly on your PostgreSQL database. + +\i archive-schema-postgre.sql diff --git a/extension/archive/archive-mysql/src/main/resources/sql/migration/V3.1__add_process_instance_id_to_archive_tables.sql b/extension/archive/archive-mysql/src/main/resources/sql/migration/V3.1__add_process_instance_id_to_archive_tables.sql new file mode 100644 index 000000000..756deac66 --- /dev/null +++ b/extension/archive/archive-mysql/src/main/resources/sql/migration/V3.1__add_process_instance_id_to_archive_tables.sql @@ -0,0 +1,9 @@ +-- Add process_instance_id column to archive tables (MySQL version) + +ALTER TABLE se_task_transfer_record_archive + ADD COLUMN `process_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'process instance id' + AFTER `task_instance_id`; + +ALTER TABLE se_assignee_operation_record_archive + ADD COLUMN `process_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'process instance id' + AFTER `task_instance_id`; diff --git a/extension/archive/archive-mysql/src/main/resources/sql/migration/V3.1__add_process_instance_id_to_archive_tables_postgresql.sql b/extension/archive/archive-mysql/src/main/resources/sql/migration/V3.1__add_process_instance_id_to_archive_tables_postgresql.sql new file mode 100644 index 000000000..cdb92f29c --- /dev/null +++ b/extension/archive/archive-mysql/src/main/resources/sql/migration/V3.1__add_process_instance_id_to_archive_tables_postgresql.sql @@ -0,0 +1,7 @@ +-- Add process_instance_id column to archive tables (PostgreSQL version) + +ALTER TABLE se_task_transfer_record_archive + ADD COLUMN IF NOT EXISTS process_instance_id bigint DEFAULT NULL; + +ALTER TABLE se_assignee_operation_record_archive + ADD COLUMN IF NOT EXISTS process_instance_id bigint DEFAULT NULL; diff --git a/extension/archive/archive-mysql/src/main/resources/sql/sharding/archive-schema-sharding-mysql.sql b/extension/archive/archive-mysql/src/main/resources/sql/sharding/archive-schema-sharding-mysql.sql new file mode 100644 index 000000000..52d5ab377 --- /dev/null +++ b/extension/archive/archive-mysql/src/main/resources/sql/sharding/archive-schema-sharding-mysql.sql @@ -0,0 +1,266 @@ +-- =========================================== +-- SmartEngine archive tables - MySQL HASH partitioned version +-- All IDs from application-layer SnowflakeIdGenerator +-- Archive tables partitioned by process_instance_id (same as runtime tables) +-- se_process_instance_archive partitioned by id +-- =========================================== + +-- =========================================== +-- 1. se_process_instance_archive +-- Partition key: id +-- =========================================== +CREATE TABLE `se_process_instance_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_definition_id_and_version` varchar(128) NOT NULL COMMENT 'process definition id and version', + `process_definition_type` varchar(255) DEFAULT NULL COMMENT 'process definition type', + `status` varchar(64) NOT NULL COMMENT '1.running 2.completed 3.aborted', + `complete_time` datetime(6) DEFAULT NULL COMMENT 'process completion time', + `parent_process_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'parent process instance id', + `parent_execution_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'parent execution instance id', + `start_user_id` varchar(128) DEFAULT NULL COMMENT 'start user id', + `biz_unique_id` varchar(255) DEFAULT NULL COMMENT 'biz unique id', + `reason` varchar(255) DEFAULT NULL COMMENT 'reason', + `comment` varchar(255) DEFAULT NULL COMMENT 'comment', + `title` varchar(255) DEFAULT NULL COMMENT 'title', + `tag` varchar(255) DEFAULT NULL COMMENT 'tag', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`), + KEY `idx_archive_pi_tenant_status` (`tenant_id`, `status`), + KEY `idx_archive_pi_archived_time` (`archived_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Process instance archive table' +PARTITION BY HASH (`id`) PARTITIONS 16; + +-- =========================================== +-- 2. se_task_instance_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE `se_task_instance_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `process_definition_id_and_version` varchar(128) DEFAULT NULL COMMENT 'process definition id and version', + `process_definition_type` varchar(255) DEFAULT NULL COMMENT 'process definition type', + `activity_instance_id` bigint(20) unsigned NOT NULL COMMENT 'activity instance id', + `process_definition_activity_id` varchar(255) NOT NULL COMMENT 'process definition activity id', + `execution_instance_id` bigint(20) unsigned NOT NULL COMMENT 'execution instance id', + `claim_user_id` varchar(255) DEFAULT NULL COMMENT 'claim user id', + `title` varchar(255) DEFAULT NULL COMMENT 'title', + `priority` int(11) DEFAULT 500 COMMENT 'priority', + `tag` varchar(255) DEFAULT NULL COMMENT 'tag', + `claim_time` datetime(6) DEFAULT NULL COMMENT 'claim time', + `complete_time` datetime(6) DEFAULT NULL COMMENT 'complete time', + `status` varchar(255) NOT NULL COMMENT 'status', + `comment` varchar(255) DEFAULT NULL COMMENT 'comment', + `extension` varchar(255) DEFAULT NULL COMMENT 'extension', + `domain_code` varchar(64) DEFAULT NULL COMMENT 'domain code', + `extra` json DEFAULT NULL COMMENT 'extra JSON data', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`, `process_instance_id`), + KEY `idx_archive_ti_archived_time` (`archived_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Task instance archive table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; + +-- =========================================== +-- 3. se_execution_instance_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE `se_execution_instance_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `process_definition_id_and_version` varchar(255) NOT NULL COMMENT 'process definition id and version', + `process_definition_activity_id` varchar(255) NOT NULL COMMENT 'process definition activity id', + `activity_instance_id` bigint(20) unsigned NOT NULL COMMENT 'activity instance id', + `block_id` bigint(20) unsigned DEFAULT NULL COMMENT 'block_id', + `active` tinyint(4) NOT NULL COMMENT '1:active 0:inactive', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`, `process_instance_id`), + KEY `idx_archive_ei_archived_time` (`archived_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Execution instance archive table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; + +-- =========================================== +-- 4. se_activity_instance_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE `se_activity_instance_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `process_definition_id_and_version` varchar(255) NOT NULL COMMENT 'process definition id and version', + `process_definition_activity_id` varchar(64) NOT NULL COMMENT 'process definition activity id', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`, `process_instance_id`), + KEY `idx_archive_ai_archived_time` (`archived_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Activity instance archive table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; + +-- =========================================== +-- 5. se_variable_instance_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE `se_variable_instance_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `execution_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'execution instance id', + `field_key` varchar(128) NOT NULL COMMENT 'field key', + `field_type` varchar(128) NOT NULL COMMENT 'field type', + `field_double_value` decimal(65,30) DEFAULT NULL COMMENT 'field double value', + `field_long_value` bigint(20) DEFAULT NULL COMMENT 'field long value', + `field_string_value` varchar(4000) DEFAULT NULL COMMENT 'field string value', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`, `process_instance_id`), + KEY `idx_archive_vi_archived_time` (`archived_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Variable instance archive table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; + +-- =========================================== +-- 6. se_task_assignee_instance_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE `se_task_assignee_instance_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `assignee_id` varchar(255) NOT NULL COMMENT 'assignee id', + `assignee_type` varchar(128) NOT NULL COMMENT 'assignee type', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`, `process_instance_id`), + KEY `idx_archive_tai_archived_time` (`archived_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Task assignee instance archive table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; + +-- =========================================== +-- 7. se_supervision_instance_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE `se_supervision_instance_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `supervisor_user_id` varchar(255) NOT NULL COMMENT 'supervisor user id', + `supervision_reason` varchar(500) DEFAULT NULL COMMENT 'supervision reason', + `supervision_type` varchar(64) NOT NULL COMMENT 'supervision type: urge/track/remind', + `status` varchar(64) NOT NULL COMMENT 'status: active/closed', + `close_time` datetime(6) DEFAULT NULL COMMENT 'close time', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`, `process_instance_id`), + KEY `idx_archive_si_archived_time` (`archived_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Supervision instance archive table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; + +-- =========================================== +-- 8. se_notification_instance_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE `se_notification_instance_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'task instance id', + `sender_user_id` varchar(255) NOT NULL COMMENT 'sender user id', + `receiver_user_id` varchar(255) NOT NULL COMMENT 'receiver user id', + `notification_type` varchar(64) NOT NULL COMMENT 'notification type: cc/inform', + `title` varchar(255) DEFAULT NULL COMMENT 'notification title', + `content` varchar(1000) DEFAULT NULL COMMENT 'notification content', + `read_status` varchar(64) NOT NULL DEFAULT 'unread' COMMENT 'read status: unread/read', + `read_time` datetime(6) DEFAULT NULL COMMENT 'read time', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`, `process_instance_id`), + KEY `idx_archive_ni_archived_time` (`archived_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Notification instance archive table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; + +-- =========================================== +-- 9. se_task_transfer_record_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE `se_task_transfer_record_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `from_user_id` varchar(255) NOT NULL COMMENT 'from user id', + `to_user_id` varchar(255) NOT NULL COMMENT 'to user id', + `transfer_reason` varchar(500) DEFAULT NULL COMMENT 'transfer reason', + `deadline` datetime(6) DEFAULT NULL COMMENT 'processing deadline', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`, `process_instance_id`), + KEY `idx_archive_ttr_archived_time` (`archived_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Task transfer record archive table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; + +-- =========================================== +-- 10. se_assignee_operation_record_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE `se_assignee_operation_record_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `operation_type` varchar(64) NOT NULL COMMENT 'operation type: add_assignee/remove_assignee', + `operator_user_id` varchar(255) NOT NULL COMMENT 'operator user id', + `target_user_id` varchar(255) NOT NULL COMMENT 'target user id', + `operation_reason` varchar(500) DEFAULT NULL COMMENT 'operation reason', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`, `process_instance_id`), + KEY `idx_archive_aor_archived_time` (`archived_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Assignee operation record archive table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; + +-- =========================================== +-- 11. se_process_rollback_record_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE `se_process_rollback_record_archive` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK (from original)', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `rollback_type` varchar(64) NOT NULL COMMENT 'rollback type: previous/specific', + `from_activity_id` varchar(255) NOT NULL COMMENT 'from activity id', + `to_activity_id` varchar(255) NOT NULL COMMENT 'to activity id', + `operator_user_id` varchar(255) NOT NULL COMMENT 'operator user id', + `rollback_reason` varchar(500) DEFAULT NULL COMMENT 'rollback reason', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + `archived_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT 'archive time', + PRIMARY KEY (`id`, `process_instance_id`), + KEY `idx_archive_prr_archived_time` (`archived_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Process rollback record archive table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; diff --git a/extension/archive/archive-mysql/src/main/resources/sql/sharding/archive-schema-sharding-postgresql.sql b/extension/archive/archive-mysql/src/main/resources/sql/sharding/archive-schema-sharding-postgresql.sql new file mode 100644 index 000000000..4649a0885 --- /dev/null +++ b/extension/archive/archive-mysql/src/main/resources/sql/sharding/archive-schema-sharding-postgresql.sql @@ -0,0 +1,453 @@ +-- =========================================== +-- SmartEngine archive tables - PostgreSQL HASH partitioned version +-- All IDs from application-layer SnowflakeIdGenerator +-- Archive tables partitioned by process_instance_id (same as runtime tables) +-- se_process_instance_archive partitioned by id +-- =========================================== + +-- =========================================== +-- 1. se_process_instance_archive +-- Partition key: id +-- =========================================== +CREATE TABLE se_process_instance_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_definition_id_and_version varchar(128) NOT NULL, + process_definition_type varchar(255), + status varchar(64) NOT NULL, + complete_time timestamp(6), + parent_process_instance_id bigint, + parent_execution_instance_id bigint, + start_user_id varchar(128), + biz_unique_id varchar(255), + reason varchar(255), + comment varchar(255), + title varchar(255), + tag varchar(255), + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +) PARTITION BY HASH (id); + +CREATE TABLE se_process_instance_archive_p00 PARTITION OF se_process_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_process_instance_archive_p01 PARTITION OF se_process_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_process_instance_archive_p02 PARTITION OF se_process_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_process_instance_archive_p03 PARTITION OF se_process_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_process_instance_archive_p04 PARTITION OF se_process_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_process_instance_archive_p05 PARTITION OF se_process_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_process_instance_archive_p06 PARTITION OF se_process_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_process_instance_archive_p07 PARTITION OF se_process_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_process_instance_archive_p08 PARTITION OF se_process_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_process_instance_archive_p09 PARTITION OF se_process_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_process_instance_archive_p10 PARTITION OF se_process_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_process_instance_archive_p11 PARTITION OF se_process_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_process_instance_archive_p12 PARTITION OF se_process_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_process_instance_archive_p13 PARTITION OF se_process_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_process_instance_archive_p14 PARTITION OF se_process_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_process_instance_archive_p15 PARTITION OF se_process_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +CREATE INDEX idx_archive_pi_tenant_status ON se_process_instance_archive (tenant_id, status); +CREATE INDEX idx_archive_pi_archived_time ON se_process_instance_archive (archived_time); + +-- =========================================== +-- 2. se_task_instance_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE se_task_instance_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + process_definition_id_and_version varchar(128), + process_definition_type varchar(255), + activity_instance_id bigint NOT NULL, + process_definition_activity_id varchar(255) NOT NULL, + execution_instance_id bigint NOT NULL, + claim_user_id varchar(255), + title varchar(255), + priority int DEFAULT 500, + tag varchar(255), + claim_time timestamp(6), + complete_time timestamp(6), + status varchar(255) NOT NULL, + comment varchar(255), + extension varchar(255), + domain_code varchar(64), + extra jsonb, + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +CREATE TABLE se_task_instance_archive_p00 PARTITION OF se_task_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_task_instance_archive_p01 PARTITION OF se_task_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_task_instance_archive_p02 PARTITION OF se_task_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_task_instance_archive_p03 PARTITION OF se_task_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_task_instance_archive_p04 PARTITION OF se_task_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_task_instance_archive_p05 PARTITION OF se_task_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_task_instance_archive_p06 PARTITION OF se_task_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_task_instance_archive_p07 PARTITION OF se_task_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_task_instance_archive_p08 PARTITION OF se_task_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_task_instance_archive_p09 PARTITION OF se_task_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_task_instance_archive_p10 PARTITION OF se_task_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_task_instance_archive_p11 PARTITION OF se_task_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_task_instance_archive_p12 PARTITION OF se_task_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_task_instance_archive_p13 PARTITION OF se_task_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_task_instance_archive_p14 PARTITION OF se_task_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_task_instance_archive_p15 PARTITION OF se_task_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +CREATE INDEX idx_archive_ti_archived_time ON se_task_instance_archive (archived_time); + +-- =========================================== +-- 3. se_execution_instance_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE se_execution_instance_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + process_definition_id_and_version varchar(255) NOT NULL, + process_definition_activity_id varchar(255) NOT NULL, + activity_instance_id bigint NOT NULL, + block_id bigint, + active smallint NOT NULL, + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +CREATE TABLE se_execution_instance_archive_p00 PARTITION OF se_execution_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_execution_instance_archive_p01 PARTITION OF se_execution_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_execution_instance_archive_p02 PARTITION OF se_execution_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_execution_instance_archive_p03 PARTITION OF se_execution_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_execution_instance_archive_p04 PARTITION OF se_execution_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_execution_instance_archive_p05 PARTITION OF se_execution_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_execution_instance_archive_p06 PARTITION OF se_execution_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_execution_instance_archive_p07 PARTITION OF se_execution_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_execution_instance_archive_p08 PARTITION OF se_execution_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_execution_instance_archive_p09 PARTITION OF se_execution_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_execution_instance_archive_p10 PARTITION OF se_execution_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_execution_instance_archive_p11 PARTITION OF se_execution_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_execution_instance_archive_p12 PARTITION OF se_execution_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_execution_instance_archive_p13 PARTITION OF se_execution_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_execution_instance_archive_p14 PARTITION OF se_execution_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_execution_instance_archive_p15 PARTITION OF se_execution_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +CREATE INDEX idx_archive_ei_archived_time ON se_execution_instance_archive (archived_time); + +-- =========================================== +-- 4. se_activity_instance_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE se_activity_instance_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + process_definition_id_and_version varchar(255) NOT NULL, + process_definition_activity_id varchar(64) NOT NULL, + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +CREATE TABLE se_activity_instance_archive_p00 PARTITION OF se_activity_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_activity_instance_archive_p01 PARTITION OF se_activity_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_activity_instance_archive_p02 PARTITION OF se_activity_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_activity_instance_archive_p03 PARTITION OF se_activity_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_activity_instance_archive_p04 PARTITION OF se_activity_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_activity_instance_archive_p05 PARTITION OF se_activity_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_activity_instance_archive_p06 PARTITION OF se_activity_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_activity_instance_archive_p07 PARTITION OF se_activity_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_activity_instance_archive_p08 PARTITION OF se_activity_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_activity_instance_archive_p09 PARTITION OF se_activity_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_activity_instance_archive_p10 PARTITION OF se_activity_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_activity_instance_archive_p11 PARTITION OF se_activity_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_activity_instance_archive_p12 PARTITION OF se_activity_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_activity_instance_archive_p13 PARTITION OF se_activity_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_activity_instance_archive_p14 PARTITION OF se_activity_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_activity_instance_archive_p15 PARTITION OF se_activity_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +CREATE INDEX idx_archive_ai_archived_time ON se_activity_instance_archive (archived_time); + +-- =========================================== +-- 5. se_variable_instance_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE se_variable_instance_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + execution_instance_id bigint, + field_key varchar(128) NOT NULL, + field_type varchar(128) NOT NULL, + field_double_value decimal(65,30), + field_long_value bigint, + field_string_value varchar(4000), + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +CREATE TABLE se_variable_instance_archive_p00 PARTITION OF se_variable_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_variable_instance_archive_p01 PARTITION OF se_variable_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_variable_instance_archive_p02 PARTITION OF se_variable_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_variable_instance_archive_p03 PARTITION OF se_variable_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_variable_instance_archive_p04 PARTITION OF se_variable_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_variable_instance_archive_p05 PARTITION OF se_variable_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_variable_instance_archive_p06 PARTITION OF se_variable_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_variable_instance_archive_p07 PARTITION OF se_variable_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_variable_instance_archive_p08 PARTITION OF se_variable_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_variable_instance_archive_p09 PARTITION OF se_variable_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_variable_instance_archive_p10 PARTITION OF se_variable_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_variable_instance_archive_p11 PARTITION OF se_variable_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_variable_instance_archive_p12 PARTITION OF se_variable_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_variable_instance_archive_p13 PARTITION OF se_variable_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_variable_instance_archive_p14 PARTITION OF se_variable_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_variable_instance_archive_p15 PARTITION OF se_variable_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +CREATE INDEX idx_archive_vi_archived_time ON se_variable_instance_archive (archived_time); + +-- =========================================== +-- 6. se_task_assignee_instance_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE se_task_assignee_instance_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint NOT NULL, + assignee_id varchar(255) NOT NULL, + assignee_type varchar(128) NOT NULL, + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +CREATE TABLE se_task_assignee_instance_archive_p00 PARTITION OF se_task_assignee_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_task_assignee_instance_archive_p01 PARTITION OF se_task_assignee_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_task_assignee_instance_archive_p02 PARTITION OF se_task_assignee_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_task_assignee_instance_archive_p03 PARTITION OF se_task_assignee_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_task_assignee_instance_archive_p04 PARTITION OF se_task_assignee_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_task_assignee_instance_archive_p05 PARTITION OF se_task_assignee_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_task_assignee_instance_archive_p06 PARTITION OF se_task_assignee_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_task_assignee_instance_archive_p07 PARTITION OF se_task_assignee_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_task_assignee_instance_archive_p08 PARTITION OF se_task_assignee_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_task_assignee_instance_archive_p09 PARTITION OF se_task_assignee_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_task_assignee_instance_archive_p10 PARTITION OF se_task_assignee_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_task_assignee_instance_archive_p11 PARTITION OF se_task_assignee_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_task_assignee_instance_archive_p12 PARTITION OF se_task_assignee_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_task_assignee_instance_archive_p13 PARTITION OF se_task_assignee_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_task_assignee_instance_archive_p14 PARTITION OF se_task_assignee_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_task_assignee_instance_archive_p15 PARTITION OF se_task_assignee_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +CREATE INDEX idx_archive_tai_archived_time ON se_task_assignee_instance_archive (archived_time); + +-- =========================================== +-- 7. se_supervision_instance_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE se_supervision_instance_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint NOT NULL, + supervisor_user_id varchar(255) NOT NULL, + supervision_reason varchar(500), + supervision_type varchar(64) NOT NULL, + status varchar(64) NOT NULL, + close_time timestamp(6), + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +CREATE TABLE se_supervision_instance_archive_p00 PARTITION OF se_supervision_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_supervision_instance_archive_p01 PARTITION OF se_supervision_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_supervision_instance_archive_p02 PARTITION OF se_supervision_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_supervision_instance_archive_p03 PARTITION OF se_supervision_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_supervision_instance_archive_p04 PARTITION OF se_supervision_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_supervision_instance_archive_p05 PARTITION OF se_supervision_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_supervision_instance_archive_p06 PARTITION OF se_supervision_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_supervision_instance_archive_p07 PARTITION OF se_supervision_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_supervision_instance_archive_p08 PARTITION OF se_supervision_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_supervision_instance_archive_p09 PARTITION OF se_supervision_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_supervision_instance_archive_p10 PARTITION OF se_supervision_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_supervision_instance_archive_p11 PARTITION OF se_supervision_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_supervision_instance_archive_p12 PARTITION OF se_supervision_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_supervision_instance_archive_p13 PARTITION OF se_supervision_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_supervision_instance_archive_p14 PARTITION OF se_supervision_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_supervision_instance_archive_p15 PARTITION OF se_supervision_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +CREATE INDEX idx_archive_si_archived_time ON se_supervision_instance_archive (archived_time); + +-- =========================================== +-- 8. se_notification_instance_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE se_notification_instance_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint, + sender_user_id varchar(255) NOT NULL, + receiver_user_id varchar(255) NOT NULL, + notification_type varchar(64) NOT NULL, + title varchar(255), + content varchar(1000), + read_status varchar(64) NOT NULL DEFAULT 'unread', + read_time timestamp(6), + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +CREATE TABLE se_notification_instance_archive_p00 PARTITION OF se_notification_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_notification_instance_archive_p01 PARTITION OF se_notification_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_notification_instance_archive_p02 PARTITION OF se_notification_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_notification_instance_archive_p03 PARTITION OF se_notification_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_notification_instance_archive_p04 PARTITION OF se_notification_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_notification_instance_archive_p05 PARTITION OF se_notification_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_notification_instance_archive_p06 PARTITION OF se_notification_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_notification_instance_archive_p07 PARTITION OF se_notification_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_notification_instance_archive_p08 PARTITION OF se_notification_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_notification_instance_archive_p09 PARTITION OF se_notification_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_notification_instance_archive_p10 PARTITION OF se_notification_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_notification_instance_archive_p11 PARTITION OF se_notification_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_notification_instance_archive_p12 PARTITION OF se_notification_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_notification_instance_archive_p13 PARTITION OF se_notification_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_notification_instance_archive_p14 PARTITION OF se_notification_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_notification_instance_archive_p15 PARTITION OF se_notification_instance_archive FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +CREATE INDEX idx_archive_ni_archived_time ON se_notification_instance_archive (archived_time); + +-- =========================================== +-- 9. se_task_transfer_record_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE se_task_transfer_record_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + task_instance_id bigint NOT NULL, + process_instance_id bigint NOT NULL, + from_user_id varchar(255) NOT NULL, + to_user_id varchar(255) NOT NULL, + transfer_reason varchar(500), + deadline timestamp(6), + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +CREATE TABLE se_task_transfer_record_archive_p00 PARTITION OF se_task_transfer_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_task_transfer_record_archive_p01 PARTITION OF se_task_transfer_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_task_transfer_record_archive_p02 PARTITION OF se_task_transfer_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_task_transfer_record_archive_p03 PARTITION OF se_task_transfer_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_task_transfer_record_archive_p04 PARTITION OF se_task_transfer_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_task_transfer_record_archive_p05 PARTITION OF se_task_transfer_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_task_transfer_record_archive_p06 PARTITION OF se_task_transfer_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_task_transfer_record_archive_p07 PARTITION OF se_task_transfer_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_task_transfer_record_archive_p08 PARTITION OF se_task_transfer_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_task_transfer_record_archive_p09 PARTITION OF se_task_transfer_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_task_transfer_record_archive_p10 PARTITION OF se_task_transfer_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_task_transfer_record_archive_p11 PARTITION OF se_task_transfer_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_task_transfer_record_archive_p12 PARTITION OF se_task_transfer_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_task_transfer_record_archive_p13 PARTITION OF se_task_transfer_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_task_transfer_record_archive_p14 PARTITION OF se_task_transfer_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_task_transfer_record_archive_p15 PARTITION OF se_task_transfer_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +CREATE INDEX idx_archive_ttr_archived_time ON se_task_transfer_record_archive (archived_time); + +-- =========================================== +-- 10. se_assignee_operation_record_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE se_assignee_operation_record_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + task_instance_id bigint NOT NULL, + process_instance_id bigint NOT NULL, + operation_type varchar(64) NOT NULL, + operator_user_id varchar(255) NOT NULL, + target_user_id varchar(255) NOT NULL, + operation_reason varchar(500), + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +CREATE TABLE se_assignee_operation_record_archive_p00 PARTITION OF se_assignee_operation_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_assignee_operation_record_archive_p01 PARTITION OF se_assignee_operation_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_assignee_operation_record_archive_p02 PARTITION OF se_assignee_operation_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_assignee_operation_record_archive_p03 PARTITION OF se_assignee_operation_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_assignee_operation_record_archive_p04 PARTITION OF se_assignee_operation_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_assignee_operation_record_archive_p05 PARTITION OF se_assignee_operation_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_assignee_operation_record_archive_p06 PARTITION OF se_assignee_operation_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_assignee_operation_record_archive_p07 PARTITION OF se_assignee_operation_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_assignee_operation_record_archive_p08 PARTITION OF se_assignee_operation_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_assignee_operation_record_archive_p09 PARTITION OF se_assignee_operation_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_assignee_operation_record_archive_p10 PARTITION OF se_assignee_operation_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_assignee_operation_record_archive_p11 PARTITION OF se_assignee_operation_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_assignee_operation_record_archive_p12 PARTITION OF se_assignee_operation_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_assignee_operation_record_archive_p13 PARTITION OF se_assignee_operation_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_assignee_operation_record_archive_p14 PARTITION OF se_assignee_operation_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_assignee_operation_record_archive_p15 PARTITION OF se_assignee_operation_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +CREATE INDEX idx_archive_aor_archived_time ON se_assignee_operation_record_archive (archived_time); + +-- =========================================== +-- 11. se_process_rollback_record_archive +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE se_process_rollback_record_archive ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint NOT NULL, + rollback_type varchar(64) NOT NULL, + from_activity_id varchar(255) NOT NULL, + to_activity_id varchar(255) NOT NULL, + operator_user_id varchar(255) NOT NULL, + rollback_reason varchar(500), + tenant_id varchar(64), + archived_time timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +CREATE TABLE se_process_rollback_record_archive_p00 PARTITION OF se_process_rollback_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_process_rollback_record_archive_p01 PARTITION OF se_process_rollback_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_process_rollback_record_archive_p02 PARTITION OF se_process_rollback_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_process_rollback_record_archive_p03 PARTITION OF se_process_rollback_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_process_rollback_record_archive_p04 PARTITION OF se_process_rollback_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_process_rollback_record_archive_p05 PARTITION OF se_process_rollback_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_process_rollback_record_archive_p06 PARTITION OF se_process_rollback_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_process_rollback_record_archive_p07 PARTITION OF se_process_rollback_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_process_rollback_record_archive_p08 PARTITION OF se_process_rollback_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_process_rollback_record_archive_p09 PARTITION OF se_process_rollback_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_process_rollback_record_archive_p10 PARTITION OF se_process_rollback_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_process_rollback_record_archive_p11 PARTITION OF se_process_rollback_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_process_rollback_record_archive_p12 PARTITION OF se_process_rollback_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_process_rollback_record_archive_p13 PARTITION OF se_process_rollback_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_process_rollback_record_archive_p14 PARTITION OF se_process_rollback_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_process_rollback_record_archive_p15 PARTITION OF se_process_rollback_record_archive FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +CREATE INDEX idx_archive_prr_archived_time ON se_process_rollback_record_archive (archived_time); diff --git a/extension/archive/archive-mysql/src/test/java/com/alibaba/smart/framework/engine/archive/ArchiveServiceTest.java b/extension/archive/archive-mysql/src/test/java/com/alibaba/smart/framework/engine/archive/ArchiveServiceTest.java new file mode 100644 index 000000000..10c93fcbb --- /dev/null +++ b/extension/archive/archive-mysql/src/test/java/com/alibaba/smart/framework/engine/archive/ArchiveServiceTest.java @@ -0,0 +1,306 @@ +package com.alibaba.smart.framework.engine.archive; + +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import com.alibaba.smart.framework.engine.archive.config.ArchiveProperties; +import com.alibaba.smart.framework.engine.archive.dao.ArchiveDAO; +import com.alibaba.smart.framework.engine.archive.service.ArchiveService; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:spring/archive-test.xml") +public class ArchiveServiceTest { + + @Autowired + private ArchiveService archiveService; + + @Autowired + private ArchiveProperties archiveProperties; + + @Autowired + private ArchiveDAO archiveDAO; + + @Autowired + private DataSource dataSource; + + @Autowired + private TransactionTemplate transactionTemplate; + + private JdbcTemplate jdbcTemplate; + + @Before + public void setUp() { + jdbcTemplate = new JdbcTemplate(dataSource); + cleanupTestData(); + } + + private void cleanupTestData() { + // Clean archive tables + jdbcTemplate.execute("DELETE FROM se_task_transfer_record_archive WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_assignee_operation_record_archive WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_supervision_instance_archive WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_notification_instance_archive WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_process_rollback_record_archive WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_task_assignee_instance_archive WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_variable_instance_archive WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_task_instance_archive WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_execution_instance_archive WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_activity_instance_archive WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_process_instance_archive WHERE tenant_id = 'archive_test'"); + + // Clean runtime tables + jdbcTemplate.execute("DELETE FROM se_task_transfer_record WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_assignee_operation_record WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_supervision_instance WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_notification_instance WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_process_rollback_record WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_task_assignee_instance WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_variable_instance WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_task_instance WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_execution_instance WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_activity_instance WHERE tenant_id = 'archive_test'"); + jdbcTemplate.execute("DELETE FROM se_process_instance WHERE tenant_id = 'archive_test'"); + } + + @Test + public void testArchiveDisabled() { + archiveProperties.setEnabled(false); + try { + int count = archiveService.archive("archive_test"); + Assert.assertEquals(0, count); + } finally { + archiveProperties.setEnabled(true); + } + } + + @Test + public void testArchiveCompletedProcess() { + // Insert a completed process older than retention period + long processId = insertCompletedProcess(daysAgo(5)); + insertRelatedData(processId); + + archiveProperties.setRetentionDays(1); + int count = archiveService.archive("archive_test"); + + Assert.assertEquals(1, count); + + // Verify archived + Long archivedCount = jdbcTemplate.queryForObject( + "SELECT count(*) FROM se_process_instance_archive WHERE id = ? AND tenant_id = 'archive_test'", + Long.class, processId); + Assert.assertEquals(Long.valueOf(1), archivedCount); + + // Verify original deleted + Long originalCount = jdbcTemplate.queryForObject( + "SELECT count(*) FROM se_process_instance WHERE id = ? AND tenant_id = 'archive_test'", + Long.class, processId); + Assert.assertEquals(Long.valueOf(0), originalCount); + } + + @Test + public void testRunningProcessNotArchived() { + // Insert a running process + long processId = insertProcess("running", daysAgo(5), null); + + archiveProperties.setRetentionDays(1); + int count = archiveService.archive("archive_test"); + + Assert.assertEquals(0, count); + + // Verify still in original table + Long originalCount = jdbcTemplate.queryForObject( + "SELECT count(*) FROM se_process_instance WHERE id = ? AND tenant_id = 'archive_test'", + Long.class, processId); + Assert.assertEquals(Long.valueOf(1), originalCount); + } + + @Test + public void testRecentCompletedNotArchived() { + // Insert a recently completed process (within retention period) + long processId = insertCompletedProcess(new Date()); + + archiveProperties.setRetentionDays(30); + int count = archiveService.archive("archive_test"); + + Assert.assertEquals(0, count); + + // Verify still in original table + Long originalCount = jdbcTemplate.queryForObject( + "SELECT count(*) FROM se_process_instance WHERE id = ? AND tenant_id = 'archive_test'", + Long.class, processId); + Assert.assertEquals(Long.valueOf(1), originalCount); + } + + @Test + public void testBatchArchive() { + // Insert multiple completed processes older than retention + for (int i = 0; i < 5; i++) { + long processId = insertCompletedProcess(daysAgo(10)); + insertRelatedData(processId); + } + + archiveProperties.setRetentionDays(1); + archiveProperties.setBatchSize(2); + int count = archiveService.archive("archive_test"); + + Assert.assertEquals(5, count); + + Long archivedCount = jdbcTemplate.queryForObject( + "SELECT count(*) FROM se_process_instance_archive WHERE tenant_id = 'archive_test'", + Long.class); + Assert.assertEquals(Long.valueOf(5), archivedCount); + } + + @Test + public void testRelatedDataArchived() { + long processId = insertCompletedProcess(daysAgo(5)); + insertRelatedData(processId); + + archiveProperties.setRetentionDays(1); + archiveService.archive("archive_test"); + + // Verify all related tables have archived data + assertArchivedCount("se_task_instance_archive", "process_instance_id", processId, 1); + assertArchivedCount("se_execution_instance_archive", "process_instance_id", processId, 1); + assertArchivedCount("se_activity_instance_archive", "process_instance_id", processId, 1); + assertArchivedCount("se_variable_instance_archive", "process_instance_id", processId, 1); + assertArchivedCount("se_task_assignee_instance_archive", "process_instance_id", processId, 1); + } + + @Test + public void testOriginalDataDeleted() { + long processId = insertCompletedProcess(daysAgo(5)); + insertRelatedData(processId); + + archiveProperties.setRetentionDays(1); + archiveService.archive("archive_test"); + + // Verify original data is deleted + assertOriginalCount("se_process_instance", "id", processId, 0); + assertOriginalCount("se_task_instance", "process_instance_id", processId, 0); + assertOriginalCount("se_execution_instance", "process_instance_id", processId, 0); + assertOriginalCount("se_activity_instance", "process_instance_id", processId, 0); + assertOriginalCount("se_variable_instance", "process_instance_id", processId, 0); + assertOriginalCount("se_task_assignee_instance", "process_instance_id", processId, 0); + } + + @Test + public void testAbortedProcessArchived() { + // Insert an aborted process older than retention + long processId = insertProcess("aborted", daysAgo(10), daysAgo(5)); + insertRelatedData(processId); + + archiveProperties.setRetentionDays(1); + int count = archiveService.archive("archive_test"); + + Assert.assertEquals(1, count); + + Long archivedCount = jdbcTemplate.queryForObject( + "SELECT count(*) FROM se_process_instance_archive WHERE id = ? AND tenant_id = 'archive_test'", + Long.class, processId); + Assert.assertEquals(Long.valueOf(1), archivedCount); + } + + // ========== Helper methods ========== + + private long insertCompletedProcess(Date completeTime) { + return insertProcess("completed", daysAgo(30), completeTime); + } + + private long insertProcess(String status, Date createTime, Date completeTime) { + Timestamp createTs = new Timestamp(createTime.getTime()); + Timestamp completeTs = completeTime != null ? new Timestamp(completeTime.getTime()) : null; + + jdbcTemplate.update( + "INSERT INTO se_process_instance (gmt_create, gmt_modified, process_definition_id_and_version, " + + "status, complete_time, tenant_id) VALUES (?, ?, ?, ?, ?, ?)", + createTs, createTs, "test:1.0", status, completeTs, "archive_test"); + + return jdbcTemplate.queryForObject( + "SELECT max(id) FROM se_process_instance WHERE tenant_id = 'archive_test'", Long.class); + } + + private void insertRelatedData(long processId) { + Timestamp now = new Timestamp(System.currentTimeMillis()); + + // activity instance + jdbcTemplate.update( + "INSERT INTO se_activity_instance (gmt_create, gmt_modified, process_instance_id, " + + "process_definition_id_and_version, process_definition_activity_id, tenant_id) " + + "VALUES (?, ?, ?, ?, ?, ?)", + now, now, processId, "test:1.0", "userTask1", "archive_test"); + + Long activityId = jdbcTemplate.queryForObject( + "SELECT max(id) FROM se_activity_instance WHERE tenant_id = 'archive_test' AND process_instance_id = ?", + Long.class, processId); + + // execution instance + jdbcTemplate.update( + "INSERT INTO se_execution_instance (gmt_create, gmt_modified, process_instance_id, " + + "process_definition_id_and_version, process_definition_activity_id, activity_instance_id, " + + "active, tenant_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + now, now, processId, "test:1.0", "userTask1", activityId, 0, "archive_test"); + + Long executionId = jdbcTemplate.queryForObject( + "SELECT max(id) FROM se_execution_instance WHERE tenant_id = 'archive_test' AND process_instance_id = ?", + Long.class, processId); + + // task instance + jdbcTemplate.update( + "INSERT INTO se_task_instance (gmt_create, gmt_modified, process_instance_id, " + + "process_definition_id_and_version, activity_instance_id, process_definition_activity_id, " + + "execution_instance_id, status, tenant_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + now, now, processId, "test:1.0", activityId, "userTask1", executionId, "completed", "archive_test"); + + Long taskId = jdbcTemplate.queryForObject( + "SELECT max(id) FROM se_task_instance WHERE tenant_id = 'archive_test' AND process_instance_id = ?", + Long.class, processId); + + // variable instance + jdbcTemplate.update( + "INSERT INTO se_variable_instance (gmt_create, gmt_modified, process_instance_id, " + + "execution_instance_id, field_key, field_type, field_string_value, tenant_id) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + now, now, processId, executionId, "testVar", "string", "testValue", "archive_test"); + + // task assignee instance + jdbcTemplate.update( + "INSERT INTO se_task_assignee_instance (gmt_create, gmt_modified, process_instance_id, " + + "task_instance_id, assignee_id, assignee_type, tenant_id) VALUES (?, ?, ?, ?, ?, ?, ?)", + now, now, processId, taskId, "user1", "user", "archive_test"); + } + + private Date daysAgo(int days) { + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DAY_OF_MONTH, -days); + return cal.getTime(); + } + + private void assertArchivedCount(String table, String column, long id, int expected) { + Long count = jdbcTemplate.queryForObject( + "SELECT count(*) FROM " + table + " WHERE " + column + " = ? AND tenant_id = 'archive_test'", + Long.class, id); + Assert.assertEquals("Expected " + expected + " rows in " + table, Long.valueOf(expected), count); + } + + private void assertOriginalCount(String table, String column, long id, int expected) { + Long count = jdbcTemplate.queryForObject( + "SELECT count(*) FROM " + table + " WHERE " + column + " = ? AND tenant_id = 'archive_test'", + Long.class, id); + Assert.assertEquals("Expected " + expected + " rows in " + table, Long.valueOf(expected), count); + } +} diff --git a/extension/archive/archive-mysql/src/test/resources/log4j.properties b/extension/archive/archive-mysql/src/test/resources/log4j.properties new file mode 100644 index 000000000..36ea1d152 --- /dev/null +++ b/extension/archive/archive-mysql/src/test/resources/log4j.properties @@ -0,0 +1,6 @@ +log4j.rootLogger=INFO,stdout +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n +log4j.logger.com.alibaba.smart.framework.engine.archive=DEBUG diff --git a/extension/archive/archive-mysql/src/test/resources/mybatis/mybatis-archive-test-config.xml b/extension/archive/archive-mysql/src/test/resources/mybatis/mybatis-archive-test-config.xml new file mode 100644 index 000000000..e364f616f --- /dev/null +++ b/extension/archive/archive-mysql/src/test/resources/mybatis/mybatis-archive-test-config.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/extension/archive/archive-mysql/src/test/resources/spring/archive-test.xml b/extension/archive/archive-mysql/src/test/resources/spring/archive-test.xml new file mode 100644 index 000000000..6dd70d430 --- /dev/null +++ b/extension/archive/archive-mysql/src/test/resources/spring/archive-test.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extension/retry/retry-common/pom.xml b/extension/retry/retry-common/pom.xml index 3ade22f00..341bf5be5 100644 --- a/extension/retry/retry-common/pom.xml +++ b/extension/retry/retry-common/pom.xml @@ -3,7 +3,7 @@ smart-engine com.alibaba.smart.framework - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT ../../../pom.xml diff --git a/extension/retry/retry-custom/pom.xml b/extension/retry/retry-custom/pom.xml index 33b74d929..eb611b133 100644 --- a/extension/retry/retry-custom/pom.xml +++ b/extension/retry/retry-custom/pom.xml @@ -3,7 +3,7 @@ smart-engine com.alibaba.smart.framework - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT ../../../pom.xml diff --git a/extension/retry/retry-mysql/pom.xml b/extension/retry/retry-mysql/pom.xml index f5c0ab10b..288d23d2d 100644 --- a/extension/retry/retry-mysql/pom.xml +++ b/extension/retry/retry-mysql/pom.xml @@ -3,7 +3,7 @@ smart-engine com.alibaba.smart.framework - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT ../../../pom.xml diff --git a/extension/retry/retry-mysql/src/main/resources/mybatis/sqlmap/retry_record.xml b/extension/retry/retry-mysql/src/main/resources/mybatis/sqlmap/retry_record.xml index add0976a8..5cde96f3d 100644 --- a/extension/retry/retry-mysql/src/main/resources/mybatis/sqlmap/retry_record.xml +++ b/extension/retry/retry-mysql/src/main/resources/mybatis/sqlmap/retry_record.xml @@ -10,7 +10,7 @@ insert into se_retry_record() - values (#{id}, now(), now(), #{instanceId}, #{retryTimes},#{retrySuccess},#{requestParams}) + values (#{id}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, #{instanceId}, #{retryTimes},#{retrySuccess},#{requestParams}) @@ -20,7 +20,7 @@ update se_retry_record - gmt_modified=now() + gmt_modified=CURRENT_TIMESTAMP ,retrySuccess = #{retrySuccess} ,retryTimes = #{retryTimes} diff --git a/extension/storage/storage-common/pom.xml b/extension/storage/storage-common/pom.xml index a76953056..62813f040 100644 --- a/extension/storage/storage-common/pom.xml +++ b/extension/storage/storage-common/pom.xml @@ -3,7 +3,7 @@ smart-engine com.alibaba.smart.framework - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT ../../../pom.xml diff --git a/extension/storage/storage-custom/pom.xml b/extension/storage/storage-custom/pom.xml index 298735f66..bd1092311 100644 --- a/extension/storage/storage-custom/pom.xml +++ b/extension/storage/storage-custom/pom.xml @@ -5,7 +5,7 @@ com.alibaba.smart.framework smart-engine - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT ../../../pom.xml diff --git a/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomActivityInstanceStorage.java b/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomActivityInstanceStorage.java index 620bc9c63..9f83bb668 100644 --- a/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomActivityInstanceStorage.java +++ b/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomActivityInstanceStorage.java @@ -17,7 +17,7 @@ /** * Created by 高海军 帝奇 74394 on 2017 February 11:54. */ -@ExtensionBinding(group = ExtensionConstant.COMMON, bindKey = ActivityInstanceStorage.class) +@ExtensionBinding(group = ExtensionConstant.CUSTOM, bindKey = ActivityInstanceStorage.class) public class CustomActivityInstanceStorage implements ActivityInstanceStorage { diff --git a/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomExecutionInstanceStorage.java b/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomExecutionInstanceStorage.java index f9e43a0f0..b862f5e9d 100644 --- a/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomExecutionInstanceStorage.java +++ b/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomExecutionInstanceStorage.java @@ -21,7 +21,7 @@ /** * Created by 高海军 帝奇 74394 on 2017 February 11:54. */ -@ExtensionBinding(group = ExtensionConstant.COMMON, bindKey = ExecutionInstanceStorage.class) +@ExtensionBinding(group = ExtensionConstant.CUSTOM, bindKey = ExecutionInstanceStorage.class) public class CustomExecutionInstanceStorage implements ExecutionInstanceStorage { @Override diff --git a/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomProcessInstanceStorage.java b/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomProcessInstanceStorage.java index fd1817863..e3dac0471 100644 --- a/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomProcessInstanceStorage.java +++ b/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomProcessInstanceStorage.java @@ -19,7 +19,7 @@ /** * Created by 高海军 帝奇 74394 on 2017 February 11:54. */ -@ExtensionBinding(group = ExtensionConstant.COMMON, bindKey = ProcessInstanceStorage.class) +@ExtensionBinding(group = ExtensionConstant.CUSTOM, bindKey = ProcessInstanceStorage.class) public class CustomProcessInstanceStorage implements ProcessInstanceStorage { diff --git a/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomTaskAssigneeInstanceStorage.java b/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomTaskAssigneeInstanceStorage.java index 33c3951ba..b173d842e 100644 --- a/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomTaskAssigneeInstanceStorage.java +++ b/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomTaskAssigneeInstanceStorage.java @@ -12,7 +12,7 @@ import com.alibaba.smart.framework.engine.service.param.query.PendingTaskQueryParam; import static com.alibaba.smart.framework.engine.persister.common.constant.StorageConstant.NOT_IMPLEMENT_INTENTIONALLY; -@ExtensionBinding(group = ExtensionConstant.COMMON, bindKey = TaskAssigneeStorage.class) +@ExtensionBinding(group = ExtensionConstant.CUSTOM, bindKey = TaskAssigneeStorage.class) public class CustomTaskAssigneeInstanceStorage implements TaskAssigneeStorage { diff --git a/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomTaskInstanceStorage.java b/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomTaskInstanceStorage.java index 69a7e3ae1..0d9c8ef72 100644 --- a/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomTaskInstanceStorage.java +++ b/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomTaskInstanceStorage.java @@ -18,7 +18,7 @@ * Created by 高海军 帝奇 74394 on 2017 February 11:54. */ -@ExtensionBinding(group = ExtensionConstant.COMMON, bindKey = TaskInstanceStorage.class) +@ExtensionBinding(group = ExtensionConstant.CUSTOM, bindKey = TaskInstanceStorage.class) public class CustomTaskInstanceStorage implements TaskInstanceStorage { diff --git a/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomVariableInstanceStorage.java b/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomVariableInstanceStorage.java index 08b7cfeb0..b18ffec71 100644 --- a/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomVariableInstanceStorage.java +++ b/extension/storage/storage-custom/src/main/java/com/alibaba/smart/framework/engine/persister/custom/CustomVariableInstanceStorage.java @@ -12,7 +12,7 @@ /** * Created by 高海军 帝奇 74394 on 2017 October 16:54. */ -@ExtensionBinding(group = ExtensionConstant.COMMON, bindKey = VariableInstanceStorage.class) +@ExtensionBinding(group = ExtensionConstant.CUSTOM, bindKey = VariableInstanceStorage.class) public class CustomVariableInstanceStorage implements VariableInstanceStorage { diff --git a/extension/storage/storage-custom/src/test/java/com/alibaba/smart/framework/engine/test/query/FluentQueryApiTest.java b/extension/storage/storage-custom/src/test/java/com/alibaba/smart/framework/engine/test/query/FluentQueryApiTest.java new file mode 100644 index 000000000..a548f7ed3 --- /dev/null +++ b/extension/storage/storage-custom/src/test/java/com/alibaba/smart/framework/engine/test/query/FluentQueryApiTest.java @@ -0,0 +1,218 @@ +package com.alibaba.smart.framework.engine.test.query; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.alibaba.smart.framework.engine.SmartEngine; +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultSmartEngine; +import com.alibaba.smart.framework.engine.constant.TaskInstanceConstant; +import com.alibaba.smart.framework.engine.model.assembly.ProcessDefinition; +import com.alibaba.smart.framework.engine.model.instance.ProcessInstance; +import com.alibaba.smart.framework.engine.model.instance.TaskInstance; +import com.alibaba.smart.framework.engine.persister.custom.session.PersisterSession; +import com.alibaba.smart.framework.engine.query.TaskQuery; +import com.alibaba.smart.framework.engine.query.ProcessInstanceQuery; +import com.alibaba.smart.framework.engine.query.SupervisionQuery; +import com.alibaba.smart.framework.engine.query.NotificationQuery; +import com.alibaba.smart.framework.engine.service.command.ProcessCommandService; +import com.alibaba.smart.framework.engine.service.command.RepositoryCommandService; +import com.alibaba.smart.framework.engine.service.command.TaskCommandService; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * Integration tests for Fluent Query API. + * + * @author SmartEngine Team + */ +public class FluentQueryApiTest { + + protected ProcessEngineConfiguration processEngineConfiguration; + protected SmartEngine smartEngine; + protected RepositoryCommandService repositoryCommandService; + protected ProcessCommandService processCommandService; + protected TaskCommandService taskCommandService; + + @Before + public void setUp() { + PersisterSession.create(); + + processEngineConfiguration = new DefaultProcessEngineConfiguration(); + smartEngine = new DefaultSmartEngine(); + smartEngine.init(processEngineConfiguration); + + repositoryCommandService = smartEngine.getRepositoryCommandService(); + processCommandService = smartEngine.getProcessCommandService(); + taskCommandService = smartEngine.getTaskCommandService(); + } + + @After + public void tearDown() { + PersisterSession.destroySession(); + } + + @Test + public void testCreateTaskQuery() { + // Deploy a simple process + ProcessDefinition processDefinition = repositoryCommandService + .deploy("basic-process.bpmn.xml").getFirstProcessDefinition(); + + // Start process instance + Map variables = new HashMap(); + variables.put("route", "a"); + ProcessInstance processInstance = processCommandService.start( + processDefinition.getId(), processDefinition.getVersion(), variables); + + PersisterSession.currentSession().putProcessInstance(processInstance); + + // Test fluent query API + TaskQuery query = smartEngine.createTaskQuery(); + Assert.assertNotNull(query); + + // Query with process instance ID - returns empty list since basic-process uses receiveTask not userTask + List tasks = query + .processInstanceId(processInstance.getInstanceId()) + .list(); + + Assert.assertNotNull(tasks); + } + + @Test + public void testTaskQueryChaining() { + TaskQuery query = smartEngine.createTaskQuery(); + + // Test method chaining returns same query object + TaskQuery chainedQuery = query + .processInstanceId("123") + .taskAssignee("user001") + .taskStatus(TaskInstanceConstant.PENDING) + .taskPriority(5) + .pageOffset(0) + .pageSize(10) + .tenantId("tenant001") + .orderByCreateTime() + .desc(); + + Assert.assertSame(query, chainedQuery); + } + + @Test + public void testProcessInstanceQueryChaining() { + ProcessInstanceQuery query = smartEngine.createProcessQuery(); + + // Test method chaining returns same query object + ProcessInstanceQuery chainedQuery = query + .processInstanceId("123") + .startedBy("user001") + .processStatus("running") + .processDefinitionType("approval") + .bizUniqueId("biz001") + .pageOffset(0) + .pageSize(20) + .tenantId("tenant001") + .orderByStartTime() + .desc(); + + Assert.assertSame(query, chainedQuery); + } + + @Test + public void testTaskQueryWithOrdering() { + TaskQuery query = smartEngine.createTaskQuery(); + + // Test multiple order by + query + .orderByPriority().desc() + .orderByCreateTime().asc(); + + // Should not throw exception + Assert.assertNotNull(query); + } + + @Test + public void testProcessInstanceQueryCount() { + // Deploy a simple process + ProcessDefinition processDefinition = repositoryCommandService + .deploy("basic-process.bpmn.xml").getFirstProcessDefinition(); + + // Start process instance + Map variables = new HashMap(); + variables.put("route", "a"); + ProcessInstance processInstance = processCommandService.start( + processDefinition.getId(), processDefinition.getVersion(), variables); + + PersisterSession.currentSession().putProcessInstance(processInstance); + + // Count should work without errors + long count = smartEngine.createProcessQuery() + .processInstanceId(processInstance.getInstanceId()) + .count(); + + Assert.assertTrue(count >= 0); + } + + @Test + public void testTaskQueryListPage() { + TaskQuery query = smartEngine.createTaskQuery(); + + // Test listPage method + List tasks = query + .taskStatus(TaskInstanceConstant.PENDING) + .listPage(0, 10); + + Assert.assertNotNull(tasks); + } + + @Test + public void testProcessInstanceQuerySingleResult() { + // Deploy a simple process + ProcessDefinition processDefinition = repositoryCommandService + .deploy("basic-process.bpmn.xml").getFirstProcessDefinition(); + + // Start process instance + Map variables = new HashMap(); + variables.put("route", "a"); + ProcessInstance processInstance = processCommandService.start( + processDefinition.getId(), processDefinition.getVersion(), variables); + + PersisterSession.currentSession().putProcessInstance(processInstance); + + // Query single result + ProcessInstance result = smartEngine.createProcessQuery() + .processInstanceId(processInstance.getInstanceId()) + .singleResult(); + + Assert.assertNotNull(result); + Assert.assertEquals(processInstance.getInstanceId(), result.getInstanceId()); + } + + @Test + public void testSupervisionQueryChaining() { + // Test SupervisionQuery chaining + SupervisionQuery query = smartEngine.createSupervisionQuery() + .supervisorUserId("supervisor001") + .supervisionStatus("active") + .orderByCreateTime().desc() + .pageSize(10); + + Assert.assertNotNull(query); + } + + @Test + public void testNotificationQueryChaining() { + // Test NotificationQuery chaining + NotificationQuery query = smartEngine.createNotificationQuery() + .receiverUserId("user001") + .readStatus("unread") + .orderByCreateTime().desc() + .pageSize(20); + + Assert.assertNotNull(query); + } +} diff --git a/extension/storage/storage-custom/src/test/resources/user-task-process.bpmn20.xml b/extension/storage/storage-custom/src/test/resources/user-task-process.bpmn20.xml new file mode 100644 index 000000000..8c8abc5c6 --- /dev/null +++ b/extension/storage/storage-custom/src/test/resources/user-task-process.bpmn20.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/extension/storage/storage-dual/pom.xml b/extension/storage/storage-dual/pom.xml new file mode 100644 index 000000000..dce523134 --- /dev/null +++ b/extension/storage/storage-dual/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + + com.alibaba.smart.framework + smart-engine + 3.7.0-SNAPSHOT + ../../../pom.xml + + + smart-engine-extension-storage-dual + Smart Engine Extension Storage Dual + + + + ${project.groupId} + smart-engine-core + ${project.version} + + + + ${project.groupId} + smart-engine-extension-storage-mysql + ${project.version} + + + + ${project.groupId} + smart-engine-extension-storage-custom + ${project.version} + + + + org.mvel + mvel2 + + + + + org.aspectj + aspectjweaver + + + aopalliance + aopalliance + + + org.aspectj + aspectjrt + + + + + org.mybatis + mybatis + + + org.mybatis + mybatis-spring + + + + com.alibaba + fastjson + 1.2.83 + test + + + + + org.springframework + spring-test + + + + org.postgresql + postgresql + 42.7.4 + test + + + + org.slf4j + slf4j-log4j12 + ${slf4j.version} + test + + + + diff --git a/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/BackwardCompatibilityTest.java b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/BackwardCompatibilityTest.java new file mode 100644 index 000000000..4a00b075a --- /dev/null +++ b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/BackwardCompatibilityTest.java @@ -0,0 +1,66 @@ +package com.alibaba.smart.framework.engine.test.storage.dual; + +import com.alibaba.smart.framework.engine.SmartEngine; +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultSmartEngine; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.extension.scanner.SimpleAnnotationScanner; +import com.alibaba.smart.framework.engine.instance.storage.ProcessInstanceStorage; + +import org.junit.After; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + * Verify backward compatibility: when StorageRouter is null (not configured), + * the scanner falls back to original behavior. + */ +public class BackwardCompatibilityTest { + + private SimpleAnnotationScanner scanner; + + @After + public void tearDown() { + if (scanner != null) { + scanner.clear(); + } + } + + @Test + public void testScannerWithoutStorageRouterFallsBackToOriginal() { + // Manually create scanner without StorageRouter + scanner = new SimpleAnnotationScanner(SmartEngine.class.getPackage().getName()); + // storageRouter is null by default + + ProcessEngineConfiguration config = new DefaultProcessEngineConfiguration(); + config.setAnnotationScanner(scanner); + + SmartEngine engine = new DefaultSmartEngine(); + + // Set StorageRouter to null to simulate old behavior + config.setStorageRouter(null); + + engine.init(config); + + // SERVICE group lookups should still work (non-storage types) + Object servicePoint = scanner.getExtensionPoint(ExtensionConstant.SERVICE, + com.alibaba.smart.framework.engine.service.command.RepositoryCommandService.class); + assertNotNull("SERVICE group extension point should still work", servicePoint); + } + + @Test + public void testNonStorageExtensionPointsUnaffected() { + ProcessEngineConfiguration config = new DefaultProcessEngineConfiguration(); + SmartEngine engine = new DefaultSmartEngine(); + engine.init(config); + + // SERVICE group should work normally (these are not routed through StorageRouter) + Object repoService = config.getAnnotationScanner() + .getExtensionPoint(ExtensionConstant.SERVICE, + com.alibaba.smart.framework.engine.service.command.ProcessCommandService.class); + assertNotNull("ProcessCommandService should be resolved via original mechanism", repoService); + } +} diff --git a/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/DefaultDatabaseModeTest.java b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/DefaultDatabaseModeTest.java new file mode 100644 index 000000000..bb814f349 --- /dev/null +++ b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/DefaultDatabaseModeTest.java @@ -0,0 +1,63 @@ +package com.alibaba.smart.framework.engine.test.storage.dual; + +import com.alibaba.smart.framework.engine.SmartEngine; +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultSmartEngine; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.instance.storage.ProcessInstanceStorage; +import com.alibaba.smart.framework.engine.storage.StorageMode; +import com.alibaba.smart.framework.engine.storage.StorageModeHolder; +import com.alibaba.smart.framework.engine.storage.StorageRouter; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Verify that the default mode is DATABASE and the scanner returns + * database storage implementations. + */ +public class DefaultDatabaseModeTest { + + private ProcessEngineConfiguration processEngineConfiguration; + private SmartEngine smartEngine; + + @Before + public void setUp() { + processEngineConfiguration = new DefaultProcessEngineConfiguration(); + smartEngine = new DefaultSmartEngine(); + smartEngine.init(processEngineConfiguration); + } + + @After + public void tearDown() { + StorageModeHolder.clearAll(); + if (processEngineConfiguration != null) { + processEngineConfiguration.getAnnotationScanner().clear(); + } + } + + @Test + public void testDefaultModeIsDatabase() { + StorageRouter router = processEngineConfiguration.getStorageRouter(); + assertEquals("Default mode should be DATABASE", StorageMode.DATABASE, router.getDefaultMode()); + assertEquals("Resolved mode should be DATABASE when no ThreadLocal set", + StorageMode.DATABASE, router.resolveCurrentMode()); + } + + @Test + public void testScannerReturnsDatabaseStorageByDefault() { + // When no ThreadLocal is set, getExtensionPoint should return DATABASE storage + ProcessInstanceStorage storage = processEngineConfiguration.getAnnotationScanner() + .getExtensionPoint(ExtensionConstant.COMMON, ProcessInstanceStorage.class); + + assertNotNull("Storage should not be null", storage); + assertTrue("Default storage should be database implementation (not Custom)", + !storage.getClass().getSimpleName().startsWith("Custom")); + } +} diff --git a/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/DualModeProcessTest.java b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/DualModeProcessTest.java new file mode 100644 index 000000000..1825f7cc9 --- /dev/null +++ b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/DualModeProcessTest.java @@ -0,0 +1,246 @@ +package com.alibaba.smart.framework.engine.test.storage.dual; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.alibaba.smart.framework.engine.SmartEngine; +import com.alibaba.smart.framework.engine.configuration.ConfigurationOption; +import com.alibaba.smart.framework.engine.configuration.InstanceAccessor; +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultSmartEngine; +import com.alibaba.smart.framework.engine.configuration.scanner.AnnotationScanner; +import com.alibaba.smart.framework.engine.extension.scanner.SimpleAnnotationScanner; +import com.alibaba.smart.framework.engine.model.assembly.ProcessDefinition; +import com.alibaba.smart.framework.engine.model.instance.ActivityInstance; +import com.alibaba.smart.framework.engine.model.instance.ExecutionInstance; +import com.alibaba.smart.framework.engine.model.instance.InstanceStatus; +import com.alibaba.smart.framework.engine.model.instance.ProcessInstance; +import com.alibaba.smart.framework.engine.model.instance.TaskInstance; +import com.alibaba.smart.framework.engine.persister.custom.session.PersisterSession; +import com.alibaba.smart.framework.engine.service.command.ProcessCommandService; +import com.alibaba.smart.framework.engine.service.command.RepositoryCommandService; +import com.alibaba.smart.framework.engine.service.command.TaskCommandService; +import com.alibaba.smart.framework.engine.service.query.ActivityQueryService; +import com.alibaba.smart.framework.engine.service.query.ExecutionQueryService; +import com.alibaba.smart.framework.engine.service.query.ProcessQueryService; +import com.alibaba.smart.framework.engine.service.query.TaskQueryService; +import com.alibaba.smart.framework.engine.storage.StorageMode; +import com.alibaba.smart.framework.engine.storage.StorageModeHolder; +import com.alibaba.smart.framework.engine.test.storage.dual.helper.BasicServiceTaskDelegation; +import com.alibaba.smart.framework.engine.test.storage.dual.helper.DefaultTaskAssigneeDispatcher; +import com.alibaba.smart.framework.engine.test.storage.dual.helper.TimeBasedIdGenerator; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Service; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Dual-mode integration test: verify that DATABASE and CUSTOM storage modes + * both work correctly within a single engine instance and a single test method. + * + *

Phase 1 (DATABASE): runs an audit process with user tasks, persisted to PostgreSQL. + *

Phase 2 (CUSTOM): runs a basic service-task process, persisted in-memory via PersisterSession. + */ +@ContextConfiguration("/spring/application-test.xml") +@RunWith(SpringJUnit4ClassRunner.class) +@Transactional +@Service +public class DualModeProcessTest implements ApplicationContextAware { + + private ApplicationContext applicationContext; + + private ProcessEngineConfiguration processEngineConfiguration; + private SmartEngine smartEngine; + private AnnotationScanner annotationScanner; + + private RepositoryCommandService repositoryCommandService; + private ProcessCommandService processCommandService; + private ProcessQueryService processQueryService; + private ExecutionQueryService executionQueryService; + private TaskCommandService taskCommandService; + private TaskQueryService taskQueryService; + private ActivityQueryService activityQueryService; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Before + public void setUp() { + annotationScanner = new SimpleAnnotationScanner(SmartEngine.class.getPackage().getName()); + + processEngineConfiguration = new DefaultProcessEngineConfiguration(); + processEngineConfiguration.setIdGenerator(new TimeBasedIdGenerator()); + processEngineConfiguration.setInstanceAccessor(new DualModeInstanceAccessor()); + processEngineConfiguration.setTaskAssigneeDispatcher(new DefaultTaskAssigneeDispatcher()); + processEngineConfiguration.getOptionContainer().put(ConfigurationOption.TRANSFER_ENABLED_OPTION); + + smartEngine = new DefaultSmartEngine(); + smartEngine.init(processEngineConfiguration); + + repositoryCommandService = smartEngine.getRepositoryCommandService(); + processCommandService = smartEngine.getProcessCommandService(); + processQueryService = smartEngine.getProcessQueryService(); + executionQueryService = smartEngine.getExecutionQueryService(); + taskCommandService = smartEngine.getTaskCommandService(); + taskQueryService = smartEngine.getTaskQueryService(); + activityQueryService = smartEngine.getActivityQueryService(); + } + + @After + public void tearDown() { + StorageModeHolder.clearAll(); + if (annotationScanner != null) { + annotationScanner.clear(); + } + } + + /** + * Single method that exercises BOTH storage modes in sequence, + * proving they coexist and switch correctly within one JVM process. + */ + @Test + public void testBothModesInSingleMethod() throws Exception { + + // ================================================================ + // Phase 1: DATABASE MODE — Audit process with user tasks + // ================================================================ + StorageModeHolder.set(StorageMode.DATABASE); + try { + // Deploy audit BPMN + ProcessDefinition auditDef = repositoryCommandService + .deploy("test-usertask-and-servicetask-exclusive.bpmn20.xml") + .getFirstProcessDefinition(); + assertEquals(17, auditDef.getBaseElementList().size()); + + // Start process + ProcessInstance auditProcess = processCommandService.start( + auditDef.getId(), auditDef.getVersion()); + assertNotNull(auditProcess); + + // Complete submit task + List submitTasks = taskQueryService + .findAllPendingTaskList(auditProcess.getInstanceId()); + assertEquals(1, submitTasks.size()); + TaskInstance submitTask = submitTasks.get(0); + + Map submitForm = new HashMap<>(); + submitForm.put("title", "dual-mode-test"); + submitForm.put("qps", "500"); + submitForm.put("capacity", "20g"); + submitForm.put("assigneeUserId", "1"); + taskCommandService.complete(submitTask.getInstanceId(), submitForm); + + // Complete audit task (approve) + List auditTasks = taskQueryService + .findAllPendingTaskList(auditProcess.getInstanceId()); + assertEquals(1, auditTasks.size()); + TaskInstance auditTask = auditTasks.get(0); + + Map approveForm = new HashMap<>(); + approveForm.put("approve", "agree"); + approveForm.put("desc", "approved in dual-mode test"); + taskCommandService.complete(auditTask.getInstanceId(), approveForm); + + // Verify process completed + ProcessInstance finalAuditProcess = processQueryService + .findById(auditProcess.getInstanceId()); + assertEquals(InstanceStatus.completed, finalAuditProcess.getStatus()); + + // Verify activity instances + List activityInstances = activityQueryService + .findAll(auditProcess.getInstanceId()); + Assert.assertEquals(6, activityInstances.size()); + assertNotNull(activityInstances.get(0).getStartTime()); + + } finally { + StorageModeHolder.clear(); + } + + // ================================================================ + // Phase 2: CUSTOM MODE — Basic service-task process in-memory + // ================================================================ + PersisterSession.create(); + StorageModeHolder.set(StorageMode.CUSTOM); + try { + BasicServiceTaskDelegation.resetCounter(); + + // Deploy basic BPMN + ProcessDefinition basicDef = repositoryCommandService + .deploy("first-smart-editor.xml") + .getFirstProcessDefinition(); + assertEquals(12, basicDef.getBaseElementList().size()); + + // Start process (a=2 → takes the service task branch → completes) + Map request = new HashMap<>(); + request.put("a", 2); + + ProcessInstance memProcess = processCommandService.start( + basicDef.getId(), basicDef.getVersion(), request); + assertNotNull(memProcess); + PersisterSession.currentSession().putProcessInstance(memProcess); + + // Verify process completed + List executions = executionQueryService + .findActiveExecutionList(memProcess.getInstanceId()); + assertEquals(0, executions.size()); + assertEquals(InstanceStatus.completed, memProcess.getStatus()); + assertEquals(1L, BasicServiceTaskDelegation.getCounter().longValue()); + + // Start another process (a=-1 → takes the receive task branch → stays running) + BasicServiceTaskDelegation.resetCounter(); + Map request2 = new HashMap<>(); + request2.put("a", -1); + + ProcessInstance memProcess2 = processCommandService.start( + basicDef.getId(), basicDef.getVersion(), request2); + assertNotNull(memProcess2); + PersisterSession.currentSession().putProcessInstance(memProcess2); + + List executions2 = executionQueryService + .findActiveExecutionList(memProcess2.getInstanceId()); + assertEquals(1, executions2.size()); + assertEquals(InstanceStatus.running, memProcess2.getStatus()); + assertEquals(0L, BasicServiceTaskDelegation.getCounter().longValue()); + + } finally { + StorageModeHolder.clear(); + PersisterSession.destroySession(); + } + } + + /** + * Accessor that tries Spring bean lookup first (for short bean names like + * "auditProcessServiceTaskDelegation"), then falls back to direct class + * instantiation (for fully-qualified class names used by in-memory BPMN). + */ + private class DualModeInstanceAccessor implements InstanceAccessor { + @Override + public Object access(String name) { + try { + return applicationContext.getBean(name); + } catch (Exception e) { + try { + return Class.forName(name).getDeclaredConstructor().newInstance(); + } catch (Exception ex) { + throw new RuntimeException("Cannot resolve delegation: " + name, ex); + } + } + } + } +} diff --git a/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/DualStorageEngineInitTest.java b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/DualStorageEngineInitTest.java new file mode 100644 index 000000000..fb8a3fa1e --- /dev/null +++ b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/DualStorageEngineInitTest.java @@ -0,0 +1,49 @@ +package com.alibaba.smart.framework.engine.test.storage.dual; + +import com.alibaba.smart.framework.engine.SmartEngine; +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultSmartEngine; +import com.alibaba.smart.framework.engine.extension.scanner.SimpleAnnotationScanner; +import com.alibaba.smart.framework.engine.storage.StorageRouter; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; + +/** + * Verify that the engine initializes successfully when both storage-mysql + * and storage-custom modules are on the classpath. + */ +public class DualStorageEngineInitTest { + + protected SimpleAnnotationScanner annotationScanner; + protected ProcessEngineConfiguration processEngineConfiguration; + protected SmartEngine smartEngine; + + @Before + public void setUp() { + processEngineConfiguration = new DefaultProcessEngineConfiguration(); + smartEngine = new DefaultSmartEngine(); + smartEngine.init(processEngineConfiguration); + } + + @After + public void tearDown() { + if (processEngineConfiguration != null) { + processEngineConfiguration.getAnnotationScanner().clear(); + } + } + + @Test + public void testEngineInitWithDualStorage() { + assertNotNull("SmartEngine should be initialized", smartEngine); + assertNotNull("ProcessEngineConfiguration should be set", + smartEngine.getProcessEngineConfiguration()); + + StorageRouter router = processEngineConfiguration.getStorageRouter(); + assertNotNull("StorageRouter should be initialized", router); + } +} diff --git a/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/MemoryModeTest.java b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/MemoryModeTest.java new file mode 100644 index 000000000..9ab4fddfe --- /dev/null +++ b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/MemoryModeTest.java @@ -0,0 +1,78 @@ +package com.alibaba.smart.framework.engine.test.storage.dual; + +import com.alibaba.smart.framework.engine.SmartEngine; +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultSmartEngine; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.instance.storage.ProcessInstanceStorage; +import com.alibaba.smart.framework.engine.storage.StorageMode; +import com.alibaba.smart.framework.engine.storage.StorageModeHolder; +import com.alibaba.smart.framework.engine.storage.StorageRouter; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Verify that when default mode is set to CUSTOM, + * the scanner returns memory (custom) storage implementations. + */ +public class MemoryModeTest { + + private ProcessEngineConfiguration processEngineConfiguration; + private SmartEngine smartEngine; + + @Before + public void setUp() { + processEngineConfiguration = new DefaultProcessEngineConfiguration(); + smartEngine = new DefaultSmartEngine(); + smartEngine.init(processEngineConfiguration); + } + + @After + public void tearDown() { + StorageModeHolder.clearAll(); + if (processEngineConfiguration != null) { + processEngineConfiguration.getAnnotationScanner().clear(); + } + } + + @Test + public void testMemoryModeViaDefaultSetting() { + StorageRouter router = processEngineConfiguration.getStorageRouter(); + router.setDefaultMode(StorageMode.CUSTOM); + + assertEquals("Default mode should be CUSTOM", StorageMode.CUSTOM, router.getDefaultMode()); + + // Verify router resolves to Custom (memory) implementation + ProcessInstanceStorage storage = router.getStorage(ProcessInstanceStorage.class); + assertNotNull("Storage should not be null", storage); + assertTrue("Storage should be Custom (memory) implementation", + storage.getClass().getSimpleName().startsWith("Custom")); + + // Also verify scanner returns a non-null proxy + ProcessInstanceStorage proxyStorage = processEngineConfiguration.getAnnotationScanner() + .getExtensionPoint(ExtensionConstant.COMMON, ProcessInstanceStorage.class); + assertNotNull("Scanner should return a proxy", proxyStorage); + } + + @Test + public void testMemoryModeViaThreadLocal() { + StorageModeHolder.set(StorageMode.CUSTOM); + try { + // Verify router resolves to Custom (memory) implementation + StorageRouter router = processEngineConfiguration.getStorageRouter(); + ProcessInstanceStorage storage = router.getStorage(ProcessInstanceStorage.class); + assertNotNull("Storage should not be null", storage); + assertTrue("Storage should be Custom (memory) implementation when ThreadLocal is CUSTOM", + storage.getClass().getSimpleName().startsWith("Custom")); + } finally { + StorageModeHolder.clear(); + } + } +} diff --git a/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/StorageRouterRegistryTest.java b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/StorageRouterRegistryTest.java new file mode 100644 index 000000000..d42a90431 --- /dev/null +++ b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/StorageRouterRegistryTest.java @@ -0,0 +1,98 @@ +package com.alibaba.smart.framework.engine.test.storage.dual; + +import com.alibaba.smart.framework.engine.SmartEngine; +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultSmartEngine; +import com.alibaba.smart.framework.engine.instance.storage.ActivityInstanceStorage; +import com.alibaba.smart.framework.engine.instance.storage.ExecutionInstanceStorage; +import com.alibaba.smart.framework.engine.instance.storage.ProcessInstanceStorage; +import com.alibaba.smart.framework.engine.instance.storage.TaskAssigneeStorage; +import com.alibaba.smart.framework.engine.instance.storage.TaskInstanceStorage; +import com.alibaba.smart.framework.engine.instance.storage.VariableInstanceStorage; +import com.alibaba.smart.framework.engine.storage.StorageMode; +import com.alibaba.smart.framework.engine.storage.StorageRouter; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Verify that StorageRouter has registered both DATABASE and CUSTOM mode + * storage implementations for all 6 storage types. + */ +public class StorageRouterRegistryTest { + + private ProcessEngineConfiguration processEngineConfiguration; + private SmartEngine smartEngine; + + @Before + public void setUp() { + processEngineConfiguration = new DefaultProcessEngineConfiguration(); + smartEngine = new DefaultSmartEngine(); + smartEngine.init(processEngineConfiguration); + } + + @After + public void tearDown() { + if (processEngineConfiguration != null) { + processEngineConfiguration.getAnnotationScanner().clear(); + } + } + + @Test + public void testDatabaseModeRegistered() { + StorageRouter router = processEngineConfiguration.getStorageRouter(); + assertNotNull(router); + + assertNotNull("DATABASE ProcessInstanceStorage should be registered", + router.getStorage(ProcessInstanceStorage.class, StorageMode.DATABASE)); + assertNotNull("DATABASE ExecutionInstanceStorage should be registered", + router.getStorage(ExecutionInstanceStorage.class, StorageMode.DATABASE)); + assertNotNull("DATABASE ActivityInstanceStorage should be registered", + router.getStorage(ActivityInstanceStorage.class, StorageMode.DATABASE)); + assertNotNull("DATABASE TaskInstanceStorage should be registered", + router.getStorage(TaskInstanceStorage.class, StorageMode.DATABASE)); + assertNotNull("DATABASE TaskAssigneeStorage should be registered", + router.getStorage(TaskAssigneeStorage.class, StorageMode.DATABASE)); + assertNotNull("DATABASE VariableInstanceStorage should be registered", + router.getStorage(VariableInstanceStorage.class, StorageMode.DATABASE)); + } + + @Test + public void testCustomModeRegistered() { + StorageRouter router = processEngineConfiguration.getStorageRouter(); + assertNotNull(router); + + assertNotNull("CUSTOMProcessInstanceStorage should be registered", + router.getStorage(ProcessInstanceStorage.class, StorageMode.CUSTOM)); + assertNotNull("CUSTOMExecutionInstanceStorage should be registered", + router.getStorage(ExecutionInstanceStorage.class, StorageMode.CUSTOM)); + assertNotNull("CUSTOMActivityInstanceStorage should be registered", + router.getStorage(ActivityInstanceStorage.class, StorageMode.CUSTOM)); + assertNotNull("CUSTOMTaskInstanceStorage should be registered", + router.getStorage(TaskInstanceStorage.class, StorageMode.CUSTOM)); + assertNotNull("CUSTOMTaskAssigneeStorage should be registered", + router.getStorage(TaskAssigneeStorage.class, StorageMode.CUSTOM)); + assertNotNull("CUSTOMVariableInstanceStorage should be registered", + router.getStorage(VariableInstanceStorage.class, StorageMode.CUSTOM)); + } + + @Test + public void testDatabaseAndCustomAreDifferentInstances() { + StorageRouter router = processEngineConfiguration.getStorageRouter(); + + Object dbStorage = router.getStorage(ProcessInstanceStorage.class, StorageMode.DATABASE); + Object memStorage = router.getStorage(ProcessInstanceStorage.class, StorageMode.CUSTOM); + + assertTrue("DATABASE and CUSTOM ProcessInstanceStorage should be different instances", + dbStorage != memStorage); + assertTrue("DATABASE storage should not be Custom implementation", + !dbStorage.getClass().getSimpleName().startsWith("Custom")); + assertTrue("CUSTOMstorage should be Custom implementation", + memStorage.getClass().getSimpleName().startsWith("Custom")); + } +} diff --git a/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/ThreadLocalModeSwitchTest.java b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/ThreadLocalModeSwitchTest.java new file mode 100644 index 000000000..973b6430f --- /dev/null +++ b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/ThreadLocalModeSwitchTest.java @@ -0,0 +1,86 @@ +package com.alibaba.smart.framework.engine.test.storage.dual; + +import com.alibaba.smart.framework.engine.SmartEngine; +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.configuration.impl.DefaultSmartEngine; +import com.alibaba.smart.framework.engine.instance.storage.ProcessInstanceStorage; +import com.alibaba.smart.framework.engine.storage.StorageMode; +import com.alibaba.smart.framework.engine.storage.StorageModeHolder; +import com.alibaba.smart.framework.engine.storage.StorageRouter; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Verify dynamic mode switching via StorageModeHolder ThreadLocal. + */ +public class ThreadLocalModeSwitchTest { + + private ProcessEngineConfiguration processEngineConfiguration; + private SmartEngine smartEngine; + private StorageRouter router; + + @Before + public void setUp() { + processEngineConfiguration = new DefaultProcessEngineConfiguration(); + smartEngine = new DefaultSmartEngine(); + smartEngine.init(processEngineConfiguration); + router = processEngineConfiguration.getStorageRouter(); + } + + @After + public void tearDown() { + StorageModeHolder.clearAll(); + if (processEngineConfiguration != null) { + processEngineConfiguration.getAnnotationScanner().clear(); + } + } + + @Test + public void testSwitchBetweenModes() { + // Default: DATABASE mode — router resolves to database storage + ProcessInstanceStorage dbStorage = router.getStorage(ProcessInstanceStorage.class); + assertNotNull(dbStorage); + assertTrue("Default should be database storage", + !dbStorage.getClass().getSimpleName().startsWith("Custom")); + + // Switch to CUSTOM mode — router resolves to custom storage + StorageModeHolder.set(StorageMode.CUSTOM); + try { + ProcessInstanceStorage memStorage = router.getStorage(ProcessInstanceStorage.class); + assertNotNull(memStorage); + assertTrue("Should be memory (Custom) storage after ThreadLocal switch", + memStorage.getClass().getSimpleName().startsWith("Custom")); + } finally { + StorageModeHolder.clear(); + } + + // Back to DATABASE mode after clearing ThreadLocal + ProcessInstanceStorage dbStorageAgain = router.getStorage(ProcessInstanceStorage.class); + assertNotNull(dbStorageAgain); + assertTrue("Should return to database storage after clearing ThreadLocal", + !dbStorageAgain.getClass().getSimpleName().startsWith("Custom")); + } + + @Test + public void testThreadLocalOverridesDefault() { + // Set default to CUSTOM + router.setDefaultMode(StorageMode.CUSTOM); + + // ThreadLocal overrides to DATABASE + StorageModeHolder.set(StorageMode.DATABASE); + try { + ProcessInstanceStorage storage = router.getStorage(ProcessInstanceStorage.class); + assertNotNull(storage); + assertTrue("ThreadLocal DATABASE should override default CUSTOM", + !storage.getClass().getSimpleName().startsWith("Custom")); + } finally { + StorageModeHolder.clear(); + } + } +} diff --git a/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/helper/AuditProcessServiceTaskDelegation.java b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/helper/AuditProcessServiceTaskDelegation.java new file mode 100644 index 000000000..4f869a9a6 --- /dev/null +++ b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/helper/AuditProcessServiceTaskDelegation.java @@ -0,0 +1,26 @@ +package com.alibaba.smart.framework.engine.test.storage.dual.helper; + +import com.alibaba.smart.framework.engine.context.ExecutionContext; +import com.alibaba.smart.framework.engine.delegation.TccDelegation; +import com.alibaba.smart.framework.engine.delegation.TccResult; + +import org.springframework.stereotype.Service; + +@Service +public class AuditProcessServiceTaskDelegation implements TccDelegation { + + @Override + public TccResult tryExecute(ExecutionContext executionContext) { + return TccResult.buildSucessfulResult(executionContext.getRequest()); + } + + @Override + public TccResult confirmExecute(ExecutionContext executionContext) { + return null; + } + + @Override + public TccResult cancelExecute(ExecutionContext executionContext) { + return null; + } +} diff --git a/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/helper/BasicServiceTaskDelegation.java b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/helper/BasicServiceTaskDelegation.java new file mode 100644 index 000000000..56030349f --- /dev/null +++ b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/helper/BasicServiceTaskDelegation.java @@ -0,0 +1,36 @@ +package com.alibaba.smart.framework.engine.test.storage.dual.helper; + +import java.util.concurrent.atomic.AtomicLong; + +import com.alibaba.smart.framework.engine.context.ExecutionContext; +import com.alibaba.smart.framework.engine.delegation.TccDelegation; +import com.alibaba.smart.framework.engine.delegation.TccResult; + +public class BasicServiceTaskDelegation implements TccDelegation { + + private static final AtomicLong counter = new AtomicLong(0); + + public static Long getCounter() { + return counter.get(); + } + + public static void resetCounter() { + counter.set(0L); + } + + @Override + public TccResult tryExecute(ExecutionContext executionContext) { + counter.addAndGet(1); + return TccResult.buildSucessfulResult(null); + } + + @Override + public TccResult confirmExecute(ExecutionContext executionContext) { + return null; + } + + @Override + public TccResult cancelExecute(ExecutionContext executionContext) { + return null; + } +} diff --git a/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/helper/DefaultTaskAssigneeDispatcher.java b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/helper/DefaultTaskAssigneeDispatcher.java new file mode 100644 index 000000000..bb47a109c --- /dev/null +++ b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/helper/DefaultTaskAssigneeDispatcher.java @@ -0,0 +1,35 @@ +package com.alibaba.smart.framework.engine.test.storage.dual.helper; + +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.smart.framework.engine.configuration.TaskAssigneeDispatcher; +import com.alibaba.smart.framework.engine.constant.AssigneeTypeConstant; +import com.alibaba.smart.framework.engine.context.ExecutionContext; +import com.alibaba.smart.framework.engine.model.assembly.Activity; +import com.alibaba.smart.framework.engine.model.instance.TaskAssigneeCandidateInstance; + +public class DefaultTaskAssigneeDispatcher implements TaskAssigneeDispatcher { + + @Override + public List getTaskAssigneeCandidateInstance(Activity activity, ExecutionContext context) { + List list = new ArrayList<>(); + + TaskAssigneeCandidateInstance c1 = new TaskAssigneeCandidateInstance(); + c1.setAssigneeId("1"); + c1.setAssigneeType(AssigneeTypeConstant.USER); + list.add(c1); + + TaskAssigneeCandidateInstance c2 = new TaskAssigneeCandidateInstance(); + c2.setAssigneeId("3"); + c2.setAssigneeType(AssigneeTypeConstant.USER); + list.add(c2); + + TaskAssigneeCandidateInstance c3 = new TaskAssigneeCandidateInstance(); + c3.setAssigneeId("5"); + c3.setAssigneeType(AssigneeTypeConstant.USER); + list.add(c3); + + return list; + } +} diff --git a/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/helper/TimeBasedIdGenerator.java b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/helper/TimeBasedIdGenerator.java new file mode 100644 index 000000000..02cb138e1 --- /dev/null +++ b/extension/storage/storage-dual/src/test/java/com/alibaba/smart/framework/engine/test/storage/dual/helper/TimeBasedIdGenerator.java @@ -0,0 +1,17 @@ +package com.alibaba.smart.framework.engine.test.storage.dual.helper; + +import java.util.concurrent.atomic.AtomicLong; + +import com.alibaba.smart.framework.engine.configuration.IdGenerator; +import com.alibaba.smart.framework.engine.model.instance.Instance; + +public class TimeBasedIdGenerator implements IdGenerator { + + private static AtomicLong temp = new AtomicLong(0); + + @Override + public void generate(Instance instance) { + String s = System.currentTimeMillis() + temp.getAndIncrement() + ""; + instance.setInstanceId(s); + } +} diff --git a/extension/storage/storage-dual/src/test/resources/first-smart-editor.xml b/extension/storage/storage-dual/src/test/resources/first-smart-editor.xml new file mode 100644 index 000000000..2eab6834c --- /dev/null +++ b/extension/storage/storage-dual/src/test/resources/first-smart-editor.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + a<1 + + + + a>1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/extension/storage/storage-dual/src/test/resources/log4j.properties b/extension/storage/storage-dual/src/test/resources/log4j.properties new file mode 100644 index 000000000..b009edaf2 --- /dev/null +++ b/extension/storage/storage-dual/src/test/resources/log4j.properties @@ -0,0 +1,6 @@ +### direct log messages to stdout ### +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.conversionPattern=[%p,%c{1},%t] %m%n +# Root logger option +log4j.rootLogger=WARN,stdout diff --git a/extension/storage/storage-dual/src/test/resources/mybatis/mybatis-test-config.xml b/extension/storage/storage-dual/src/test/resources/mybatis/mybatis-test-config.xml new file mode 100644 index 000000000..97be3cc57 --- /dev/null +++ b/extension/storage/storage-dual/src/test/resources/mybatis/mybatis-test-config.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/extension/storage/storage-dual/src/test/resources/spring/application-test.xml b/extension/storage/storage-dual/src/test/resources/spring/application-test.xml new file mode 100644 index 000000000..2bcc5cd34 --- /dev/null +++ b/extension/storage/storage-dual/src/test/resources/spring/application-test.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extension/storage/storage-dual/src/test/resources/test-usertask-and-servicetask-exclusive.bpmn20.xml b/extension/storage/storage-dual/src/test/resources/test-usertask-and-servicetask-exclusive.bpmn20.xml new file mode 100644 index 000000000..3ebbb1640 --- /dev/null +++ b/extension/storage/storage-dual/src/test/resources/test-usertask-and-servicetask-exclusive.bpmn20.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + approve == 'agree' + + + + + approve == 'upgrade' + + + + + + + + + + + + + + + + + + + + approve == 'agree' + + + + + approve == 'deny' + + + + + + + + \ No newline at end of file diff --git a/extension/storage/storage-mysql/pom.xml b/extension/storage/storage-mysql/pom.xml index 8fa40dbfb..5f2206892 100644 --- a/extension/storage/storage-mysql/pom.xml +++ b/extension/storage/storage-mysql/pom.xml @@ -5,7 +5,7 @@ com.alibaba.smart.framework smart-engine - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT ../../../pom.xml diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/AssigneeOperationRecordBuilder.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/AssigneeOperationRecordBuilder.java new file mode 100644 index 000000000..7a0ecec44 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/AssigneeOperationRecordBuilder.java @@ -0,0 +1,67 @@ +package com.alibaba.smart.framework.engine.persister.database.builder; + +import com.alibaba.smart.framework.engine.instance.impl.DefaultAssigneeOperationRecord; +import com.alibaba.smart.framework.engine.model.instance.AssigneeOperationRecord; +import com.alibaba.smart.framework.engine.persister.database.entity.AssigneeOperationRecordEntity; + +/** + * 加签减签操作记录Builder - 负责Model和Entity之间的转换 + * + * @author SmartEngine Team + */ +public class AssigneeOperationRecordBuilder { + + /** + * Entity转Model + */ + public static AssigneeOperationRecord buildFromEntity(AssigneeOperationRecordEntity entity) { + if (entity == null) { + return null; + } + + DefaultAssigneeOperationRecord record = new DefaultAssigneeOperationRecord(); + record.setInstanceId(entity.getId() != null ? entity.getId().toString() : null); + record.setTaskInstanceId(entity.getTaskInstanceId() != null ? entity.getTaskInstanceId().toString() : null); + record.setProcessInstanceId(entity.getProcessInstanceId() != null ? entity.getProcessInstanceId().toString() : null); + record.setOperationType(entity.getOperationType()); + record.setOperatorUserId(entity.getOperatorUserId()); + record.setTargetUserId(entity.getTargetUserId()); + record.setOperationReason(entity.getOperationReason()); + record.setTenantId(entity.getTenantId()); + record.setStartTime(entity.getGmtCreate()); + + return record; + } + + /** + * Model转Entity + */ + public static AssigneeOperationRecordEntity buildEntityFrom(AssigneeOperationRecord record) { + if (record == null) { + return null; + } + + AssigneeOperationRecordEntity entity = new AssigneeOperationRecordEntity(); + + if (record.getInstanceId() != null) { + entity.setId(Long.valueOf(record.getInstanceId())); + } + + if (record.getTaskInstanceId() != null) { + entity.setTaskInstanceId(Long.valueOf(record.getTaskInstanceId())); + } + + if (record.getProcessInstanceId() != null) { + entity.setProcessInstanceId(Long.valueOf(record.getProcessInstanceId())); + } + + entity.setOperationType(record.getOperationType()); + entity.setOperatorUserId(record.getOperatorUserId()); + entity.setTargetUserId(record.getTargetUserId()); + entity.setOperationReason(record.getOperationReason()); + entity.setTenantId(record.getTenantId()); + entity.setGmtCreate(record.getStartTime()); + + return entity; + } +} diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/NotificationInstanceBuilder.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/NotificationInstanceBuilder.java new file mode 100644 index 000000000..7b8b6bec9 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/NotificationInstanceBuilder.java @@ -0,0 +1,95 @@ +package com.alibaba.smart.framework.engine.persister.database.builder; + +import com.alibaba.smart.framework.engine.common.util.IdConverter; +import com.alibaba.smart.framework.engine.instance.impl.DefaultNotificationInstance; +import com.alibaba.smart.framework.engine.model.instance.NotificationInstance; +import com.alibaba.smart.framework.engine.persister.database.entity.NotificationInstanceEntity; + +/** + * Notification instance builder. + * Responsible for bidirectional conversion between NotificationInstance and NotificationInstanceEntity. + * + * @author SmartEngine Team + */ +public final class NotificationInstanceBuilder { + + private NotificationInstanceBuilder() { + // Utility class, prevent instantiation + } + + /** + * Build NotificationInstance from Entity. + * + * @param entity NotificationInstanceEntity + * @return NotificationInstance, or null if entity is null + */ + public static NotificationInstance buildNotificationInstanceFromEntity(NotificationInstanceEntity entity) { + if (entity == null) { + return null; + } + + NotificationInstance instance = new DefaultNotificationInstance(); + + // ID fields with null check + if (entity.getId() != null) { + instance.setInstanceId(IdConverter.toString(entity.getId())); + } + if (entity.getProcessInstanceId() != null) { + instance.setProcessInstanceId(IdConverter.toString(entity.getProcessInstanceId())); + } + if (entity.getTaskInstanceId() != null) { + instance.setTaskInstanceId(IdConverter.toString(entity.getTaskInstanceId())); + } + + // Common fields + instance.setTenantId(entity.getTenantId()); + instance.setStartTime(entity.getGmtCreate()); + instance.setCompleteTime(entity.getGmtModified()); + + // Business fields + instance.setSenderUserId(entity.getSenderUserId()); + instance.setReceiverUserId(entity.getReceiverUserId()); + instance.setNotificationType(entity.getNotificationType()); + instance.setTitle(entity.getTitle()); + instance.setContent(entity.getContent()); + instance.setReadStatus(entity.getReadStatus()); + instance.setReadTime(entity.getReadTime()); + + return instance; + } + + /** + * Build Entity from NotificationInstance. + * + * @param instance NotificationInstance + * @return NotificationInstanceEntity, or null if instance is null + */ + public static NotificationInstanceEntity buildEntityFromNotificationInstance(NotificationInstance instance) { + if (instance == null) { + return null; + } + + NotificationInstanceEntity entity = new NotificationInstanceEntity(); + + // ID fields with safe conversion + entity.setId(IdConverter.toLong(instance.getInstanceId(), "instanceId")); + entity.setProcessInstanceId(IdConverter.toLong(instance.getProcessInstanceId(), "processInstanceId")); + entity.setTaskInstanceId(IdConverter.toLong(instance.getTaskInstanceId(), "taskInstanceId")); + + // Common fields + entity.setTenantId(instance.getTenantId()); + entity.setGmtCreate(instance.getStartTime()); + entity.setGmtModified(instance.getCompleteTime()); + + // Business fields + entity.setSenderUserId(instance.getSenderUserId()); + entity.setReceiverUserId(instance.getReceiverUserId()); + entity.setNotificationType(instance.getNotificationType()); + entity.setTitle(instance.getTitle()); + entity.setContent(instance.getContent()); + entity.setReadStatus(instance.getReadStatus()); + entity.setReadTime(instance.getReadTime()); + + return entity; + } +} diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/RollbackRecordBuilder.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/RollbackRecordBuilder.java new file mode 100644 index 000000000..c4e49c5db --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/RollbackRecordBuilder.java @@ -0,0 +1,69 @@ +package com.alibaba.smart.framework.engine.persister.database.builder; + +import com.alibaba.smart.framework.engine.instance.impl.DefaultRollbackRecord; +import com.alibaba.smart.framework.engine.model.instance.RollbackRecord; +import com.alibaba.smart.framework.engine.persister.database.entity.RollbackRecordEntity; + +/** + * 流程回退记录Builder - 负责Model和Entity之间的转换 + * + * @author SmartEngine Team + */ +public class RollbackRecordBuilder { + + /** + * Entity转Model + */ + public static RollbackRecord buildFromEntity(RollbackRecordEntity entity) { + if (entity == null) { + return null; + } + + DefaultRollbackRecord record = new DefaultRollbackRecord(); + record.setInstanceId(entity.getId() != null ? entity.getId().toString() : null); + record.setProcessInstanceId(entity.getProcessInstanceId() != null ? entity.getProcessInstanceId().toString() : null); + record.setTaskInstanceId(entity.getTaskInstanceId() != null ? entity.getTaskInstanceId().toString() : null); + record.setRollbackType(entity.getRollbackType()); + record.setFromActivityId(entity.getFromActivityId()); + record.setToActivityId(entity.getToActivityId()); + record.setOperatorUserId(entity.getOperatorUserId()); + record.setRollbackReason(entity.getRollbackReason()); + record.setTenantId(entity.getTenantId()); + record.setStartTime(entity.getGmtCreate()); + + return record; + } + + /** + * Model转Entity + */ + public static RollbackRecordEntity buildEntityFrom(RollbackRecord record) { + if (record == null) { + return null; + } + + RollbackRecordEntity entity = new RollbackRecordEntity(); + + if (record.getInstanceId() != null) { + entity.setId(Long.valueOf(record.getInstanceId())); + } + + if (record.getProcessInstanceId() != null) { + entity.setProcessInstanceId(Long.valueOf(record.getProcessInstanceId())); + } + + if (record.getTaskInstanceId() != null) { + entity.setTaskInstanceId(Long.valueOf(record.getTaskInstanceId())); + } + + entity.setRollbackType(record.getRollbackType()); + entity.setFromActivityId(record.getFromActivityId()); + entity.setToActivityId(record.getToActivityId()); + entity.setOperatorUserId(record.getOperatorUserId()); + entity.setRollbackReason(record.getRollbackReason()); + entity.setTenantId(record.getTenantId()); + entity.setGmtCreate(record.getStartTime()); + + return entity; + } +} diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/SupervisionInstanceBuilder.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/SupervisionInstanceBuilder.java new file mode 100644 index 000000000..709f271cc --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/SupervisionInstanceBuilder.java @@ -0,0 +1,91 @@ +package com.alibaba.smart.framework.engine.persister.database.builder; + +import com.alibaba.smart.framework.engine.common.util.IdConverter; +import com.alibaba.smart.framework.engine.instance.impl.DefaultSupervisionInstance; +import com.alibaba.smart.framework.engine.model.instance.SupervisionInstance; +import com.alibaba.smart.framework.engine.persister.database.entity.SupervisionInstanceEntity; + +/** + * Supervision instance builder. + * Responsible for bidirectional conversion between SupervisionInstance and SupervisionInstanceEntity. + * + * @author SmartEngine Team + */ +public final class SupervisionInstanceBuilder { + + private SupervisionInstanceBuilder() { + // Utility class, prevent instantiation + } + + /** + * Build SupervisionInstance from Entity. + * + * @param entity SupervisionInstanceEntity + * @return SupervisionInstance, or null if entity is null + */ + public static SupervisionInstance buildSupervisionInstanceFromEntity(SupervisionInstanceEntity entity) { + if (entity == null) { + return null; + } + + SupervisionInstance instance = new DefaultSupervisionInstance(); + + // ID fields with null check + if (entity.getId() != null) { + instance.setInstanceId(IdConverter.toString(entity.getId())); + } + if (entity.getProcessInstanceId() != null) { + instance.setProcessInstanceId(IdConverter.toString(entity.getProcessInstanceId())); + } + if (entity.getTaskInstanceId() != null) { + instance.setTaskInstanceId(IdConverter.toString(entity.getTaskInstanceId())); + } + + // Common fields + instance.setTenantId(entity.getTenantId()); + instance.setStartTime(entity.getGmtCreate()); + instance.setCompleteTime(entity.getGmtModified()); + + // Business fields + instance.setSupervisorUserId(entity.getSupervisorUserId()); + instance.setSupervisionReason(entity.getSupervisionReason()); + instance.setSupervisionType(entity.getSupervisionType()); + instance.setStatus(entity.getStatus()); + instance.setCloseTime(entity.getCloseTime()); + + return instance; + } + + /** + * Build Entity from SupervisionInstance. + * + * @param instance SupervisionInstance + * @return SupervisionInstanceEntity, or null if instance is null + */ + public static SupervisionInstanceEntity buildEntityFromSupervisionInstance(SupervisionInstance instance) { + if (instance == null) { + return null; + } + + SupervisionInstanceEntity entity = new SupervisionInstanceEntity(); + + // ID fields with safe conversion + entity.setId(IdConverter.toLong(instance.getInstanceId(), "instanceId")); + entity.setProcessInstanceId(IdConverter.toLong(instance.getProcessInstanceId(), "processInstanceId")); + entity.setTaskInstanceId(IdConverter.toLong(instance.getTaskInstanceId(), "taskInstanceId")); + + // Common fields + entity.setTenantId(instance.getTenantId()); + entity.setGmtCreate(instance.getStartTime()); + entity.setGmtModified(instance.getCompleteTime()); + + // Business fields + entity.setSupervisorUserId(instance.getSupervisorUserId()); + entity.setSupervisionReason(instance.getSupervisionReason()); + entity.setSupervisionType(instance.getSupervisionType()); + entity.setStatus(instance.getStatus()); + entity.setCloseTime(instance.getCloseTime()); + + return entity; + } +} diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/TaskInstanceBuilder.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/TaskInstanceBuilder.java index 1380ed673..4f0b88b42 100644 --- a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/TaskInstanceBuilder.java +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/TaskInstanceBuilder.java @@ -59,6 +59,8 @@ public static TaskInstance buildTaskInstanceFromEntity(TaskInstanceEntity taskIn taskInstance.setClaimTime(taskInstanceEntity.getClaimTime()); taskInstance.setComment(taskInstanceEntity.getComment()); taskInstance.setExtension(taskInstanceEntity.getExtension()); + taskInstance.setDomainCode(taskInstanceEntity.getDomainCode()); + taskInstance.setExtra(taskInstanceEntity.getExtra()); taskInstance.setTitle(taskInstanceEntity.getTitle()); taskInstance.setPriority(taskInstanceEntity.getPriority()); taskInstance.setTenantId(taskInstanceEntity.getTenantId()); @@ -86,6 +88,8 @@ public static TaskInstanceEntity buildTaskInstanceEntity(TaskInstance taskInstan taskInstanceEntity.setComment(taskInstance.getComment()); taskInstanceEntity.setTitle(taskInstance.getTitle()); taskInstanceEntity.setExtension(taskInstance.getExtension()); + taskInstanceEntity.setDomainCode(taskInstance.getDomainCode()); + taskInstanceEntity.setExtra(taskInstance.getExtra()); taskInstanceEntity.setTenantId(taskInstance.getTenantId()); //taskInstanceEntity.setGmtModified(taskInstance.getCompleteTime()); diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/TaskTransferRecordBuilder.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/TaskTransferRecordBuilder.java new file mode 100644 index 000000000..9b7d1d7f0 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/builder/TaskTransferRecordBuilder.java @@ -0,0 +1,67 @@ +package com.alibaba.smart.framework.engine.persister.database.builder; + +import com.alibaba.smart.framework.engine.instance.impl.DefaultTaskTransferRecord; +import com.alibaba.smart.framework.engine.model.instance.TaskTransferRecord; +import com.alibaba.smart.framework.engine.persister.database.entity.TaskTransferRecordEntity; + +/** + * 任务移交记录Builder - 负责Model和Entity之间的转换 + * + * @author SmartEngine Team + */ +public class TaskTransferRecordBuilder { + + /** + * Entity转Model + */ + public static TaskTransferRecord buildFromEntity(TaskTransferRecordEntity entity) { + if (entity == null) { + return null; + } + + DefaultTaskTransferRecord record = new DefaultTaskTransferRecord(); + record.setInstanceId(entity.getId() != null ? entity.getId().toString() : null); + record.setTaskInstanceId(entity.getTaskInstanceId() != null ? entity.getTaskInstanceId().toString() : null); + record.setProcessInstanceId(entity.getProcessInstanceId() != null ? entity.getProcessInstanceId().toString() : null); + record.setFromUserId(entity.getFromUserId()); + record.setToUserId(entity.getToUserId()); + record.setTransferReason(entity.getTransferReason()); + record.setDeadline(entity.getDeadline()); + record.setTenantId(entity.getTenantId()); + record.setStartTime(entity.getGmtCreate()); + + return record; + } + + /** + * Model转Entity + */ + public static TaskTransferRecordEntity buildEntityFrom(TaskTransferRecord record) { + if (record == null) { + return null; + } + + TaskTransferRecordEntity entity = new TaskTransferRecordEntity(); + + if (record.getInstanceId() != null) { + entity.setId(Long.valueOf(record.getInstanceId())); + } + + if (record.getTaskInstanceId() != null) { + entity.setTaskInstanceId(Long.valueOf(record.getTaskInstanceId())); + } + + if (record.getProcessInstanceId() != null) { + entity.setProcessInstanceId(Long.valueOf(record.getProcessInstanceId())); + } + + entity.setFromUserId(record.getFromUserId()); + entity.setToUserId(record.getToUserId()); + entity.setTransferReason(record.getTransferReason()); + entity.setDeadline(record.getDeadline()); + entity.setTenantId(record.getTenantId()); + entity.setGmtCreate(record.getStartTime()); + + return entity; + } +} diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/AssigneeOperationRecordDAO.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/AssigneeOperationRecordDAO.java new file mode 100644 index 000000000..81851ef69 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/AssigneeOperationRecordDAO.java @@ -0,0 +1,25 @@ +package com.alibaba.smart.framework.engine.persister.database.dao; + +import java.util.List; + +import com.alibaba.smart.framework.engine.persister.database.entity.AssigneeOperationRecordEntity; + +import org.apache.ibatis.annotations.Param; + +/** + * AssigneeOperationRecordDAO interface + * + * @author SmartEngine Team + */ +public interface AssigneeOperationRecordDAO { + + void insert(AssigneeOperationRecordEntity entity); + + AssigneeOperationRecordEntity select(@Param("id") Long id, @Param("tenantId") String tenantId); + + List selectByTaskInstanceId(@Param("taskInstanceId") Long taskInstanceId, @Param("tenantId") String tenantId); + + void update(AssigneeOperationRecordEntity entity); + + void delete(@Param("id") Long id, @Param("tenantId") String tenantId); +} diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/NotificationInstanceDAO.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/NotificationInstanceDAO.java new file mode 100644 index 000000000..8608fc88c --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/NotificationInstanceDAO.java @@ -0,0 +1,86 @@ +package com.alibaba.smart.framework.engine.persister.database.dao; + +import java.util.List; + +import com.alibaba.smart.framework.engine.persister.database.entity.NotificationInstanceEntity; +import com.alibaba.smart.framework.engine.service.param.query.NotificationQueryParam; + +import org.apache.ibatis.annotations.Param; + +/** + * 知会抄送DAO接口 + * + * @author SmartEngine Team + */ +public interface NotificationInstanceDAO { + + /** + * 插入知会记录 + */ + void insert(NotificationInstanceEntity notificationInstanceEntity); + + /** + * 更新知会记录 + */ + int update(@Param("notificationInstanceEntity") NotificationInstanceEntity notificationInstanceEntity); + + /** + * 根据ID查询知会记录 + */ + NotificationInstanceEntity findOne(@Param("id") Long id, @Param("tenantId") String tenantId); + + /** + * 根据接收人查询知会列表 + */ + List findByReceiver(@Param("receiverUserId") String receiverUserId, + @Param("readStatus") String readStatus, + @Param("tenantId") String tenantId, + @Param("pageOffset") Integer pageOffset, + @Param("pageSize") Integer pageSize); + + /** + * 统计接收人的知会数量 + */ + Integer countByReceiver(@Param("receiverUserId") String receiverUserId, + @Param("readStatus") String readStatus, + @Param("tenantId") String tenantId); + + /** + * 根据发送人查询知会列表 + */ + List findBySender(@Param("senderUserId") String senderUserId, + @Param("tenantId") String tenantId, + @Param("pageOffset") Integer pageOffset, + @Param("pageSize") Integer pageSize); + + /** + * 统计发送人的知会数量 + */ + Integer countBySender(@Param("senderUserId") String senderUserId, + @Param("tenantId") String tenantId); + + /** + * 标记为已读 + */ + int markAsRead(@Param("id") Long id, @Param("tenantId") String tenantId); + + /** + * 批量标记为已读 + */ + int batchMarkAsRead(@Param("ids") List ids, @Param("tenantId") String tenantId); + + /** + * 删除知会记录 + */ + void delete(@Param("id") Long id, @Param("tenantId") String tenantId); + + /** + * 根据查询参数综合查询知会记录 + */ + List findByQuery(NotificationQueryParam param); + + /** + * 根据查询参数统计知会记录数量 + */ + Integer countByQuery(NotificationQueryParam param); +} \ No newline at end of file diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/ProcessRollbackRecordDAO.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/ProcessRollbackRecordDAO.java new file mode 100644 index 000000000..32f80c792 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/ProcessRollbackRecordDAO.java @@ -0,0 +1,56 @@ +package com.alibaba.smart.framework.engine.persister.database.dao; + +import java.util.List; + +import com.alibaba.smart.framework.engine.persister.database.entity.ProcessRollbackRecordEntity; + +import org.apache.ibatis.annotations.Param; + +/** + * 流程回退记录DAO接口 + * + * @author SmartEngine Team + */ +public interface ProcessRollbackRecordDAO { + + /** + * 插入回退记录 + */ + void insert(ProcessRollbackRecordEntity processRollbackRecordEntity); + + /** + * 根据ID查询回退记录 + */ + ProcessRollbackRecordEntity findOne(@Param("id") Long id, @Param("tenantId") String tenantId); + + /** + * 根据流程实例ID查询回退记录 + */ + List findByProcessInstanceId(@Param("processInstanceId") Long processInstanceId, + @Param("tenantId") String tenantId); + + /** + * 根据任务ID查询回退记录 + */ + List findByTaskId(@Param("taskInstanceId") Long taskInstanceId, + @Param("tenantId") String tenantId); + + /** + * 根据操作人查询回退记录 + */ + List findByOperator(@Param("operatorUserId") String operatorUserId, + @Param("tenantId") String tenantId, + @Param("pageOffset") Integer pageOffset, + @Param("pageSize") Integer pageSize); + + /** + * 统计操作人的回退记录数量 + */ + Integer countByOperator(@Param("operatorUserId") String operatorUserId, + @Param("tenantId") String tenantId); + + /** + * 删除回退记录 + */ + void delete(@Param("id") Long id, @Param("tenantId") String tenantId); +} \ No newline at end of file diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/RollbackRecordDAO.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/RollbackRecordDAO.java new file mode 100644 index 000000000..8b2c7fd09 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/RollbackRecordDAO.java @@ -0,0 +1,25 @@ +package com.alibaba.smart.framework.engine.persister.database.dao; + +import java.util.List; + +import com.alibaba.smart.framework.engine.persister.database.entity.RollbackRecordEntity; + +import org.apache.ibatis.annotations.Param; + +/** + * RollbackRecordDAO interface + * + * @author SmartEngine Team + */ +public interface RollbackRecordDAO { + + void insert(RollbackRecordEntity entity); + + RollbackRecordEntity select(@Param("id") Long id, @Param("tenantId") String tenantId); + + List selectByProcessInstanceId(@Param("processInstanceId") Long processInstanceId, @Param("tenantId") String tenantId); + + void update(RollbackRecordEntity entity); + + void delete(@Param("id") Long id, @Param("tenantId") String tenantId); +} diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/SupervisionInstanceDAO.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/SupervisionInstanceDAO.java new file mode 100644 index 000000000..e6d91bd46 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/SupervisionInstanceDAO.java @@ -0,0 +1,76 @@ +package com.alibaba.smart.framework.engine.persister.database.dao; + +import java.util.List; + +import com.alibaba.smart.framework.engine.persister.database.entity.SupervisionInstanceEntity; +import com.alibaba.smart.framework.engine.service.param.query.SupervisionQueryParam; + +import org.apache.ibatis.annotations.Param; + +/** + * 督办记录DAO接口 + * + * @author SmartEngine Team + */ +public interface SupervisionInstanceDAO { + + /** + * 插入督办记录 + */ + void insert(SupervisionInstanceEntity supervisionInstanceEntity); + + /** + * 更新督办记录 + */ + int update(@Param("supervisionInstanceEntity") SupervisionInstanceEntity supervisionInstanceEntity); + + /** + * 根据ID查询督办记录 + */ + SupervisionInstanceEntity findOne(@Param("id") Long id, @Param("tenantId") String tenantId); + + /** + * 根据任务ID查询活跃的督办记录 + */ + List findActiveByTaskId(@Param("taskInstanceId") Long taskInstanceId, + @Param("tenantId") String tenantId); + + /** + * 根据督办人查询督办记录 + */ + List findBySupervisor(@Param("supervisorUserId") String supervisorUserId, + @Param("tenantId") String tenantId, + @Param("pageOffset") Integer pageOffset, + @Param("pageSize") Integer pageSize); + + /** + * 统计督办记录数量 + */ + Integer countBySupervisor(@Param("supervisorUserId") String supervisorUserId, + @Param("tenantId") String tenantId); + + /** + * 关闭督办记录 + */ + int closeSupervision(@Param("id") Long id, @Param("tenantId") String tenantId); + + /** + * 根据任务ID批量关闭督办记录 + */ + int closeByTaskId(@Param("taskInstanceId") Long taskInstanceId, @Param("tenantId") String tenantId); + + /** + * 删除督办记录 + */ + void delete(@Param("id") Long id, @Param("tenantId") String tenantId); + + /** + * 根据查询参数综合查询督办记录 + */ + List findByQuery(SupervisionQueryParam param); + + /** + * 根据查询参数统计督办记录数量 + */ + Integer countByQuery(SupervisionQueryParam param); +} \ No newline at end of file diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/TaskTransferRecordDAO.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/TaskTransferRecordDAO.java new file mode 100644 index 000000000..3fca6f2c2 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/TaskTransferRecordDAO.java @@ -0,0 +1,25 @@ +package com.alibaba.smart.framework.engine.persister.database.dao; + +import java.util.List; + +import com.alibaba.smart.framework.engine.persister.database.entity.TaskTransferRecordEntity; + +import org.apache.ibatis.annotations.Param; + +/** + * TaskTransferRecordDAO interface + * + * @author SmartEngine Team + */ +public interface TaskTransferRecordDAO { + + void insert(TaskTransferRecordEntity entity); + + TaskTransferRecordEntity select(@Param("id") Long id, @Param("tenantId") String tenantId); + + List selectByTaskInstanceId(@Param("taskInstanceId") Long taskInstanceId, @Param("tenantId") String tenantId); + + void update(TaskTransferRecordEntity entity); + + void delete(@Param("id") Long id, @Param("tenantId") String tenantId); +} diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/UserNotificationIndexDAO.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/UserNotificationIndexDAO.java new file mode 100644 index 000000000..ce0b01105 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/UserNotificationIndexDAO.java @@ -0,0 +1,22 @@ +package com.alibaba.smart.framework.engine.persister.database.dao; + +import java.util.List; + +import com.alibaba.smart.framework.engine.persister.database.entity.UserNotificationIndexEntity; + +import org.apache.ibatis.annotations.Param; + +public interface UserNotificationIndexDAO { + + void insert(UserNotificationIndexEntity entity); + + void deleteByNotificationId(@Param("notificationId") Long notificationId, @Param("tenantId") String tenantId); + + void updateReadStatus(@Param("notificationId") Long notificationId, @Param("readStatus") String readStatus, @Param("tenantId") String tenantId); + + void batchUpdateReadStatus(@Param("notificationIds") List notificationIds, @Param("readStatus") String readStatus, @Param("tenantId") String tenantId); + + List findByReceiver(@Param("receiverUserId") String receiverUserId, @Param("readStatus") String readStatus, @Param("tenantId") String tenantId, @Param("pageOffset") Integer pageOffset, @Param("pageSize") Integer pageSize); + + Integer countByReceiver(@Param("receiverUserId") String receiverUserId, @Param("readStatus") String readStatus, @Param("tenantId") String tenantId); +} diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/UserTaskIndexDAO.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/UserTaskIndexDAO.java new file mode 100644 index 000000000..d0e1f01e5 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/dao/UserTaskIndexDAO.java @@ -0,0 +1,25 @@ +package com.alibaba.smart.framework.engine.persister.database.dao; + +import java.util.List; + +import com.alibaba.smart.framework.engine.persister.database.entity.UserTaskIndexEntity; +import com.alibaba.smart.framework.engine.service.param.query.TaskInstanceQueryByAssigneeParam; + +import org.apache.ibatis.annotations.Param; + +public interface UserTaskIndexDAO { + + void insert(UserTaskIndexEntity entity); + + void deleteByTaskInstanceId(@Param("taskInstanceId") Long taskInstanceId, @Param("tenantId") String tenantId); + + void deleteByAssigneeAndTask(@Param("assigneeId") String assigneeId, @Param("taskInstanceId") Long taskInstanceId, @Param("tenantId") String tenantId); + + void updateTaskStatus(@Param("taskInstanceId") Long taskInstanceId, @Param("taskStatus") String taskStatus, @Param("tenantId") String tenantId); + + void updateTaskFields(@Param("taskInstanceId") Long taskInstanceId, @Param("title") String title, @Param("priority") Integer priority, @Param("domainCode") String domainCode, @Param("tenantId") String tenantId); + + List findByAssignee(TaskInstanceQueryByAssigneeParam param); + + Integer countByAssignee(TaskInstanceQueryByAssigneeParam param); +} diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/AssigneeOperationRecordEntity.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/AssigneeOperationRecordEntity.java new file mode 100644 index 000000000..49c5ced1d --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/AssigneeOperationRecordEntity.java @@ -0,0 +1,102 @@ +package com.alibaba.smart.framework.engine.persister.database.entity; + +import java.util.Date; + +/** + * 加签减签操作记录实体 + * + * @author SmartEngine Team + */ +public class AssigneeOperationRecordEntity { + + private Long id; + private Date gmtCreate; + private Date gmtModified; + private Long taskInstanceId; + private Long processInstanceId; + private String operationType; // add_assignee, remove_assignee + private String operatorUserId; + private String targetUserId; + private String operationReason; + private String tenantId; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + public Long getTaskInstanceId() { + return taskInstanceId; + } + + public void setTaskInstanceId(Long taskInstanceId) { + this.taskInstanceId = taskInstanceId; + } + + public Long getProcessInstanceId() { + return processInstanceId; + } + + public void setProcessInstanceId(Long processInstanceId) { + this.processInstanceId = processInstanceId; + } + + public String getOperationType() { + return operationType; + } + + public void setOperationType(String operationType) { + this.operationType = operationType; + } + + public String getOperatorUserId() { + return operatorUserId; + } + + public void setOperatorUserId(String operatorUserId) { + this.operatorUserId = operatorUserId; + } + + public String getTargetUserId() { + return targetUserId; + } + + public void setTargetUserId(String targetUserId) { + this.targetUserId = targetUserId; + } + + public String getOperationReason() { + return operationReason; + } + + public void setOperationReason(String operationReason) { + this.operationReason = operationReason; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } +} \ No newline at end of file diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/NotificationInstanceEntity.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/NotificationInstanceEntity.java new file mode 100644 index 000000000..a5eea8626 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/NotificationInstanceEntity.java @@ -0,0 +1,36 @@ +package com.alibaba.smart.framework.engine.persister.database.entity; + +import java.util.Date; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 知会抄送实体类 + * + * @author SmartEngine Team + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class NotificationInstanceEntity extends BaseProcessEntity { + + private Long processInstanceId; + + private Long taskInstanceId; + + private String senderUserId; + + private String receiverUserId; + + private String notificationType; + + private String title; + + private String content; + + private String readStatus; + + private Date readTime; +} \ No newline at end of file diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/ProcessInstanceEntity.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/ProcessInstanceEntity.java index e35c05e8e..0869115c8 100644 --- a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/ProcessInstanceEntity.java +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/ProcessInstanceEntity.java @@ -19,6 +19,8 @@ public class ProcessInstanceEntity extends BaseProcessEntity { private String status; + private java.util.Date completeTime; + private String processDefinitionType; private String bizUniqueId; diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/ProcessRollbackRecordEntity.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/ProcessRollbackRecordEntity.java new file mode 100644 index 000000000..76527cd21 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/ProcessRollbackRecordEntity.java @@ -0,0 +1,30 @@ +package com.alibaba.smart.framework.engine.persister.database.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 流程回退记录实体类 + * + * @author SmartEngine Team + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProcessRollbackRecordEntity extends BaseProcessEntity { + + private Long processInstanceId; + + private Long taskInstanceId; + + private String rollbackType; + + private String fromActivityId; + + private String toActivityId; + + private String operatorUserId; + + private String rollbackReason; +} \ No newline at end of file diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/RollbackRecordEntity.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/RollbackRecordEntity.java new file mode 100644 index 000000000..d8415c5c2 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/RollbackRecordEntity.java @@ -0,0 +1,111 @@ +package com.alibaba.smart.framework.engine.persister.database.entity; + +import java.util.Date; + +/** + * 流程回退记录实体 + * + * @author SmartEngine Team + */ +public class RollbackRecordEntity { + + private Long id; + private Date gmtCreate; + private Date gmtModified; + private Long processInstanceId; + private Long taskInstanceId; + private String rollbackType; // previous, specific + private String fromActivityId; + private String toActivityId; + private String operatorUserId; + private String rollbackReason; + private String tenantId; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + public Long getProcessInstanceId() { + return processInstanceId; + } + + public void setProcessInstanceId(Long processInstanceId) { + this.processInstanceId = processInstanceId; + } + + public Long getTaskInstanceId() { + return taskInstanceId; + } + + public void setTaskInstanceId(Long taskInstanceId) { + this.taskInstanceId = taskInstanceId; + } + + public String getRollbackType() { + return rollbackType; + } + + public void setRollbackType(String rollbackType) { + this.rollbackType = rollbackType; + } + + public String getFromActivityId() { + return fromActivityId; + } + + public void setFromActivityId(String fromActivityId) { + this.fromActivityId = fromActivityId; + } + + public String getToActivityId() { + return toActivityId; + } + + public void setToActivityId(String toActivityId) { + this.toActivityId = toActivityId; + } + + public String getOperatorUserId() { + return operatorUserId; + } + + public void setOperatorUserId(String operatorUserId) { + this.operatorUserId = operatorUserId; + } + + public String getRollbackReason() { + return rollbackReason; + } + + public void setRollbackReason(String rollbackReason) { + this.rollbackReason = rollbackReason; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } +} \ No newline at end of file diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/SupervisionInstanceEntity.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/SupervisionInstanceEntity.java new file mode 100644 index 000000000..883859058 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/SupervisionInstanceEntity.java @@ -0,0 +1,32 @@ +package com.alibaba.smart.framework.engine.persister.database.entity; + +import java.util.Date; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 督办记录实体类 + * + * @author SmartEngine Team + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SupervisionInstanceEntity extends BaseProcessEntity { + + private Long processInstanceId; + + private Long taskInstanceId; + + private String supervisorUserId; + + private String supervisionReason; + + private String supervisionType; + + private String status; + + private Date closeTime; +} \ No newline at end of file diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/TaskInstanceEntity.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/TaskInstanceEntity.java index 19e0a377a..1960d5e95 100644 --- a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/TaskInstanceEntity.java +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/TaskInstanceEntity.java @@ -50,4 +50,14 @@ public class TaskInstanceEntity extends BaseProcessEntity { * 任务标题 */ private String title; + + /** + * Domain code for categorization and filtering. + */ + private String domainCode; + + /** + * Extra JSON data (stored as String in Java). + */ + private String extra; } diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/TaskTransferRecordEntity.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/TaskTransferRecordEntity.java new file mode 100644 index 000000000..6b2121e8f --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/TaskTransferRecordEntity.java @@ -0,0 +1,102 @@ +package com.alibaba.smart.framework.engine.persister.database.entity; + +import java.util.Date; + +/** + * 任务移交记录实体 + * + * @author SmartEngine Team + */ +public class TaskTransferRecordEntity { + + private Long id; + private Date gmtCreate; + private Date gmtModified; + private Long taskInstanceId; + private Long processInstanceId; + private String fromUserId; + private String toUserId; + private String transferReason; + private Date deadline; + private String tenantId; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + public Long getTaskInstanceId() { + return taskInstanceId; + } + + public void setTaskInstanceId(Long taskInstanceId) { + this.taskInstanceId = taskInstanceId; + } + + public Long getProcessInstanceId() { + return processInstanceId; + } + + public void setProcessInstanceId(Long processInstanceId) { + this.processInstanceId = processInstanceId; + } + + public String getFromUserId() { + return fromUserId; + } + + public void setFromUserId(String fromUserId) { + this.fromUserId = fromUserId; + } + + public String getToUserId() { + return toUserId; + } + + public void setToUserId(String toUserId) { + this.toUserId = toUserId; + } + + public String getTransferReason() { + return transferReason; + } + + public void setTransferReason(String transferReason) { + this.transferReason = transferReason; + } + + public Date getDeadline() { + return deadline; + } + + public void setDeadline(Date deadline) { + this.deadline = deadline; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } +} \ No newline at end of file diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/UserNotificationIndexEntity.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/UserNotificationIndexEntity.java new file mode 100644 index 000000000..e88d1c445 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/UserNotificationIndexEntity.java @@ -0,0 +1,18 @@ +package com.alibaba.smart.framework.engine.persister.database.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class UserNotificationIndexEntity extends BaseProcessEntity { + + private String receiverUserId; + private Long notificationId; + private Long processInstanceId; + private String notificationType; + private String title; + private String readStatus; +} diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/UserTaskIndexEntity.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/UserTaskIndexEntity.java new file mode 100644 index 000000000..8c9ac958e --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/entity/UserTaskIndexEntity.java @@ -0,0 +1,23 @@ +package com.alibaba.smart.framework.engine.persister.database.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class UserTaskIndexEntity extends BaseProcessEntity { + + private String assigneeId; + private String assigneeType; + private Long taskInstanceId; + private Long processInstanceId; + private String processDefinitionType; + private String domainCode; + private String extra; + private String taskStatus; + private java.util.Date taskGmtModified; + private String title; + private Integer priority; +} diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/handler/JsonTypeHandler.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/handler/JsonTypeHandler.java new file mode 100644 index 000000000..f2aeeed99 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/handler/JsonTypeHandler.java @@ -0,0 +1,41 @@ +package com.alibaba.smart.framework.engine.persister.database.handler; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; + +/** + * MyBatis TypeHandler for JSON/JSONB columns. + * Handles the type mismatch between Java String and database JSON/JSONB types + * (especially PostgreSQL JSONB which requires explicit casting). + */ +@MappedJdbcTypes(JdbcType.OTHER) +public class JsonTypeHandler extends BaseTypeHandler { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, + String parameter, JdbcType jdbcType) throws SQLException { + // Use setObject with Types.OTHER to let the JDBC driver handle JSON/JSONB conversion + ps.setObject(i, parameter, java.sql.Types.OTHER); + } + + @Override + public String getNullableResult(ResultSet rs, String columnName) throws SQLException { + return rs.getString(columnName); + } + + @Override + public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + return rs.getString(columnIndex); + } + + @Override + public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + return cs.getString(columnIndex); + } +} diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/handler/SmartIdTypeHandler.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/handler/SmartIdTypeHandler.java new file mode 100644 index 000000000..0c0e64c90 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/handler/SmartIdTypeHandler.java @@ -0,0 +1,71 @@ +package com.alibaba.smart.framework.engine.persister.database.handler; + +import java.io.Serializable; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import com.alibaba.smart.framework.engine.configuration.IdGenerator; + +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +/** + * MyBatis TypeHandler for ID fields. + * Automatically converts Serializable ID to the correct type based on IdGenerator configuration. + * + * @author SmartEngine Team + */ +public class SmartIdTypeHandler extends BaseTypeHandler { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, + Serializable parameter, JdbcType jdbcType) throws SQLException { + if (IdGenerator.getGlobalIdType() == Long.class) { + ps.setLong(i, toLong(parameter)); + } else { + ps.setString(i, parameter.toString()); + } + } + + @Override + public Serializable getNullableResult(ResultSet rs, String columnName) throws SQLException { + if (IdGenerator.getGlobalIdType() == Long.class) { + long value = rs.getLong(columnName); + return rs.wasNull() ? null : value; + } + return rs.getString(columnName); + } + + @Override + public Serializable getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + if (IdGenerator.getGlobalIdType() == Long.class) { + long value = rs.getLong(columnIndex); + return rs.wasNull() ? null : value; + } + return rs.getString(columnIndex); + } + + @Override + public Serializable getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + if (IdGenerator.getGlobalIdType() == Long.class) { + long value = cs.getLong(columnIndex); + return cs.wasNull() ? null : value; + } + return cs.getString(columnIndex); + } + + private Long toLong(Serializable value) { + if (value == null) { + return null; + } + if (value instanceof Long) { + return (Long) value; + } + if (value instanceof Number) { + return ((Number) value).longValue(); + } + return Long.valueOf(value.toString()); + } +} diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseAssigneeOperationRecordStorage.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseAssigneeOperationRecordStorage.java new file mode 100644 index 000000000..ebda14aa9 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseAssigneeOperationRecordStorage.java @@ -0,0 +1,97 @@ +package com.alibaba.smart.framework.engine.persister.database.service; + +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.extension.annotation.ExtensionBinding; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.instance.storage.AssigneeOperationRecordStorage; +import com.alibaba.smart.framework.engine.model.instance.AssigneeOperationRecord; +import com.alibaba.smart.framework.engine.persister.database.builder.AssigneeOperationRecordBuilder; +import com.alibaba.smart.framework.engine.persister.database.dao.AssigneeOperationRecordDAO; +import com.alibaba.smart.framework.engine.persister.database.entity.AssigneeOperationRecordEntity; + +/** + * 加签减签操作记录关系数据库存储实现 + * + * @author SmartEngine Team + */ +@ExtensionBinding(group = ExtensionConstant.COMMON, bindKey = AssigneeOperationRecordStorage.class) +public class RelationshipDatabaseAssigneeOperationRecordStorage implements AssigneeOperationRecordStorage { + + @Override + public AssigneeOperationRecord insert(AssigneeOperationRecord assigneeOperationRecord, + ProcessEngineConfiguration processEngineConfiguration) { + AssigneeOperationRecordDAO assigneeOperationRecordDAO = (AssigneeOperationRecordDAO) processEngineConfiguration + .getInstanceAccessor().access("assigneeOperationRecordDAO"); + + AssigneeOperationRecordEntity entity = AssigneeOperationRecordBuilder.buildEntityFrom(assigneeOperationRecord); + + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + + assigneeOperationRecordDAO.insert(entity); + + assigneeOperationRecord.setInstanceId(entity.getId().toString()); + return assigneeOperationRecord; + } + + @Override + public List findByTaskId(Long taskInstanceId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + AssigneeOperationRecordDAO assigneeOperationRecordDAO = (AssigneeOperationRecordDAO) processEngineConfiguration + .getInstanceAccessor().access("assigneeOperationRecordDAO"); + + List entityList = assigneeOperationRecordDAO + .selectByTaskInstanceId(taskInstanceId, tenantId); + + List recordList = new ArrayList<>(entityList.size()); + for (AssigneeOperationRecordEntity entity : entityList) { + AssigneeOperationRecord record = AssigneeOperationRecordBuilder.buildFromEntity(entity); + recordList.add(record); + } + + return recordList; + } + + @Override + public AssigneeOperationRecord find(Long id, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + AssigneeOperationRecordDAO assigneeOperationRecordDAO = (AssigneeOperationRecordDAO) processEngineConfiguration + .getInstanceAccessor().access("assigneeOperationRecordDAO"); + + AssigneeOperationRecordEntity entity = assigneeOperationRecordDAO.select(id, tenantId); + + if (entity == null) { + return null; + } + + return AssigneeOperationRecordBuilder.buildFromEntity(entity); + } + + @Override + public AssigneeOperationRecord update(AssigneeOperationRecord assigneeOperationRecord, + ProcessEngineConfiguration processEngineConfiguration) { + AssigneeOperationRecordDAO assigneeOperationRecordDAO = (AssigneeOperationRecordDAO) processEngineConfiguration + .getInstanceAccessor().access("assigneeOperationRecordDAO"); + + AssigneeOperationRecordEntity entity = AssigneeOperationRecordBuilder.buildEntityFrom(assigneeOperationRecord); + + entity.setGmtModified(DateUtil.getCurrentDate()); + + assigneeOperationRecordDAO.update(entity); + + return assigneeOperationRecord; + } + + @Override + public void remove(Long id, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + AssigneeOperationRecordDAO assigneeOperationRecordDAO = (AssigneeOperationRecordDAO) processEngineConfiguration + .getInstanceAccessor().access("assigneeOperationRecordDAO"); + + assigneeOperationRecordDAO.delete(id, tenantId); + } +} diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseNotificationInstanceStorage.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseNotificationInstanceStorage.java new file mode 100644 index 000000000..2afef0596 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseNotificationInstanceStorage.java @@ -0,0 +1,278 @@ +package com.alibaba.smart.framework.engine.persister.database.service; + +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.common.util.IdConverter; +import com.alibaba.smart.framework.engine.configuration.ConfigurationOption; +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.constant.NotificationConstant; +import com.alibaba.smart.framework.engine.extension.annotation.ExtensionBinding; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.instance.impl.DefaultNotificationInstance; +import com.alibaba.smart.framework.engine.instance.storage.NotificationInstanceStorage; +import com.alibaba.smart.framework.engine.model.instance.NotificationInstance; +import com.alibaba.smart.framework.engine.persister.database.builder.NotificationInstanceBuilder; +import com.alibaba.smart.framework.engine.persister.database.dao.NotificationInstanceDAO; +import com.alibaba.smart.framework.engine.persister.database.dao.UserNotificationIndexDAO; +import com.alibaba.smart.framework.engine.persister.database.entity.NotificationInstanceEntity; +import com.alibaba.smart.framework.engine.persister.database.entity.UserNotificationIndexEntity; +import com.alibaba.smart.framework.engine.service.param.query.NotificationQueryParam; + +/** + * Notification instance relational database storage implementation. + * + * @author SmartEngine Team + */ +@ExtensionBinding(group = ExtensionConstant.COMMON, bindKey = NotificationInstanceStorage.class) +public class RelationshipDatabaseNotificationInstanceStorage implements NotificationInstanceStorage { + + @Override + public NotificationInstance insert(NotificationInstance notificationInstance, + ProcessEngineConfiguration processEngineConfiguration) { + NotificationInstanceDAO notificationInstanceDAO = getDAO(processEngineConfiguration); + + NotificationInstanceEntity entity = NotificationInstanceBuilder + .buildEntityFromNotificationInstance(notificationInstance); + + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + + notificationInstanceDAO.insert(entity); + + // Set generated ID back to instance + notificationInstance.setInstanceId(IdConverter.toString(entity.getId())); + + // Write to index table in sharding mode + if (isShardingEnabled(processEngineConfiguration)) { + UserNotificationIndexDAO indexDAO = (UserNotificationIndexDAO) processEngineConfiguration + .getInstanceAccessor().access("userNotificationIndexDAO"); + UserNotificationIndexEntity indexEntity = new UserNotificationIndexEntity(); + indexEntity.setTenantId(notificationInstance.getTenantId()); + indexEntity.setReceiverUserId(notificationInstance.getReceiverUserId()); + indexEntity.setNotificationId(entity.getId()); + indexEntity.setProcessInstanceId(entity.getProcessInstanceId()); + indexEntity.setNotificationType(notificationInstance.getNotificationType()); + indexEntity.setTitle(notificationInstance.getTitle()); + indexEntity.setReadStatus(notificationInstance.getReadStatus() != null + ? notificationInstance.getReadStatus() : NotificationConstant.ReadStatus.UNREAD); + indexEntity.setGmtCreate(entity.getGmtCreate()); + indexDAO.insert(indexEntity); + } + + return notificationInstance; + } + + @Override + public NotificationInstance update(NotificationInstance notificationInstance, + ProcessEngineConfiguration processEngineConfiguration) { + NotificationInstanceDAO notificationInstanceDAO = getDAO(processEngineConfiguration); + + NotificationInstanceEntity entity = NotificationInstanceBuilder + .buildEntityFromNotificationInstance(notificationInstance); + + entity.setGmtModified(DateUtil.getCurrentDate()); + + notificationInstanceDAO.update(entity); + + return notificationInstance; + } + + @Override + public NotificationInstance find(String notificationId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + NotificationInstanceDAO notificationInstanceDAO = getDAO(processEngineConfiguration); + + Long id = IdConverter.toLong(notificationId, "notificationId"); + NotificationInstanceEntity entity = notificationInstanceDAO.findOne(id, tenantId); + + return NotificationInstanceBuilder.buildNotificationInstanceFromEntity(entity); + } + + @Override + public List findNotificationList(NotificationQueryParam param, + ProcessEngineConfiguration processEngineConfiguration) { + NotificationInstanceDAO notificationInstanceDAO = getDAO(processEngineConfiguration); + + List entityList = notificationInstanceDAO.findByQuery(param); + + return buildInstanceList(entityList); + } + + @Override + public Long countNotifications(NotificationQueryParam param, + ProcessEngineConfiguration processEngineConfiguration) { + NotificationInstanceDAO notificationInstanceDAO = getDAO(processEngineConfiguration); + + Integer count = notificationInstanceDAO.countByQuery(param); + return count != null ? count.longValue() : 0L; + } + + @Override + public Long countUnreadNotifications(String receiverUserId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + if (isShardingEnabled(processEngineConfiguration)) { + UserNotificationIndexDAO indexDAO = (UserNotificationIndexDAO) processEngineConfiguration + .getInstanceAccessor().access("userNotificationIndexDAO"); + Integer count = indexDAO.countByReceiver(receiverUserId, + NotificationConstant.ReadStatus.UNREAD, tenantId); + return count != null ? count.longValue() : 0L; + } + + NotificationInstanceDAO notificationInstanceDAO = getDAO(processEngineConfiguration); + Integer count = notificationInstanceDAO.countByReceiver(receiverUserId, + NotificationConstant.ReadStatus.UNREAD, tenantId); + return count != null ? count.longValue() : 0L; + } + + @Override + public List findByReceiver(String receiverUserId, String readStatus, + String tenantId, Integer pageOffset, Integer pageSize, + ProcessEngineConfiguration processEngineConfiguration) { + if (isShardingEnabled(processEngineConfiguration)) { + return findByReceiverFromIndex(receiverUserId, readStatus, tenantId, + pageOffset, pageSize, processEngineConfiguration); + } + + NotificationInstanceDAO notificationInstanceDAO = getDAO(processEngineConfiguration); + + List entityList = notificationInstanceDAO + .findByReceiver(receiverUserId, readStatus, tenantId, pageOffset, pageSize); + + return buildInstanceList(entityList); + } + + @Override + public List findBySender(String senderUserId, String tenantId, + Integer pageOffset, Integer pageSize, + ProcessEngineConfiguration processEngineConfiguration) { + NotificationInstanceDAO notificationInstanceDAO = getDAO(processEngineConfiguration); + + List entityList = notificationInstanceDAO + .findBySender(senderUserId, tenantId, pageOffset, pageSize); + + return buildInstanceList(entityList); + } + + @Override + public int markAsRead(String notificationId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + NotificationInstanceDAO notificationInstanceDAO = getDAO(processEngineConfiguration); + + Long id = IdConverter.toLong(notificationId, "notificationId"); + int result = notificationInstanceDAO.markAsRead(id, tenantId); + + // Update index table read status in sharding mode + if (isShardingEnabled(processEngineConfiguration) && result > 0) { + UserNotificationIndexDAO indexDAO = (UserNotificationIndexDAO) processEngineConfiguration + .getInstanceAccessor().access("userNotificationIndexDAO"); + indexDAO.updateReadStatus(id, NotificationConstant.ReadStatus.READ, tenantId); + } + + return result; + } + + @Override + public int batchMarkAsRead(List notificationIds, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + NotificationInstanceDAO notificationInstanceDAO = getDAO(processEngineConfiguration); + + List ids = IdConverter.toLongList(notificationIds); + int result = notificationInstanceDAO.batchMarkAsRead(ids, tenantId); + + // Batch update index table read status in sharding mode + if (isShardingEnabled(processEngineConfiguration) && result > 0) { + UserNotificationIndexDAO indexDAO = (UserNotificationIndexDAO) processEngineConfiguration + .getInstanceAccessor().access("userNotificationIndexDAO"); + indexDAO.batchUpdateReadStatus(ids, NotificationConstant.ReadStatus.READ, tenantId); + } + + return result; + } + + @Override + public void remove(String notificationId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + NotificationInstanceDAO notificationInstanceDAO = getDAO(processEngineConfiguration); + + Long id = IdConverter.toLong(notificationId, "notificationId"); + notificationInstanceDAO.delete(id, tenantId); + + // Delete from index table in sharding mode + if (isShardingEnabled(processEngineConfiguration)) { + UserNotificationIndexDAO indexDAO = (UserNotificationIndexDAO) processEngineConfiguration + .getInstanceAccessor().access("userNotificationIndexDAO"); + indexDAO.deleteByNotificationId(id, tenantId); + } + } + + /** + * Get DAO from configuration. + */ + private NotificationInstanceDAO getDAO(ProcessEngineConfiguration processEngineConfiguration) { + return (NotificationInstanceDAO) processEngineConfiguration + .getInstanceAccessor().access("notificationInstanceDAO"); + } + + /** + * Build instance list from entity list. + */ + private List buildInstanceList(List entityList) { + if (entityList == null || entityList.isEmpty()) { + return new ArrayList<>(); + } + + List instanceList = new ArrayList<>(entityList.size()); + for (NotificationInstanceEntity entity : entityList) { + NotificationInstance instance = NotificationInstanceBuilder.buildNotificationInstanceFromEntity(entity); + if (instance != null) { + instanceList.add(instance); + } + } + return instanceList; + } + + /** + * Find notifications by receiver from the index table (sharding mode). + */ + private List findByReceiverFromIndex(String receiverUserId, String readStatus, + String tenantId, Integer pageOffset, Integer pageSize, + ProcessEngineConfiguration processEngineConfiguration) { + UserNotificationIndexDAO indexDAO = (UserNotificationIndexDAO) processEngineConfiguration + .getInstanceAccessor().access("userNotificationIndexDAO"); + List indexEntities = indexDAO.findByReceiver( + receiverUserId, readStatus, tenantId, pageOffset, pageSize); + + if (indexEntities == null || indexEntities.isEmpty()) { + return new ArrayList<>(); + } + + List result = new ArrayList<>(indexEntities.size()); + for (UserNotificationIndexEntity indexEntity : indexEntities) { + DefaultNotificationInstance instance = new DefaultNotificationInstance(); + instance.setInstanceId(indexEntity.getNotificationId().toString()); + instance.setProcessInstanceId(indexEntity.getProcessInstanceId() != null + ? indexEntity.getProcessInstanceId().toString() : null); + instance.setReceiverUserId(indexEntity.getReceiverUserId()); + instance.setNotificationType(indexEntity.getNotificationType()); + instance.setTitle(indexEntity.getTitle()); + instance.setReadStatus(indexEntity.getReadStatus()); + instance.setTenantId(indexEntity.getTenantId()); + instance.setStartTime(indexEntity.getGmtCreate()); + result.add(instance); + } + return result; + } + + /** + * Check if sharding mode is enabled. + */ + private boolean isShardingEnabled(ProcessEngineConfiguration config) { + if (config.getOptionContainer() == null) { + return false; + } + ConfigurationOption option = config.getOptionContainer().get("shardingModeEnabled"); + return option != null && option.isEnabled(); + } +} diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseRollbackRecordStorage.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseRollbackRecordStorage.java new file mode 100644 index 000000000..ef84fd10f --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseRollbackRecordStorage.java @@ -0,0 +1,97 @@ +package com.alibaba.smart.framework.engine.persister.database.service; + +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.extension.annotation.ExtensionBinding; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.instance.storage.RollbackRecordStorage; +import com.alibaba.smart.framework.engine.model.instance.RollbackRecord; +import com.alibaba.smart.framework.engine.persister.database.builder.RollbackRecordBuilder; +import com.alibaba.smart.framework.engine.persister.database.dao.RollbackRecordDAO; +import com.alibaba.smart.framework.engine.persister.database.entity.RollbackRecordEntity; + +/** + * 流程回退记录关系数据库存储实现 + * + * @author SmartEngine Team + */ +@ExtensionBinding(group = ExtensionConstant.COMMON, bindKey = RollbackRecordStorage.class) +public class RelationshipDatabaseRollbackRecordStorage implements RollbackRecordStorage { + + @Override + public RollbackRecord insert(RollbackRecord rollbackRecord, + ProcessEngineConfiguration processEngineConfiguration) { + RollbackRecordDAO rollbackRecordDAO = (RollbackRecordDAO) processEngineConfiguration + .getInstanceAccessor().access("rollbackRecordDAO"); + + RollbackRecordEntity entity = RollbackRecordBuilder.buildEntityFrom(rollbackRecord); + + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + + rollbackRecordDAO.insert(entity); + + rollbackRecord.setInstanceId(entity.getId().toString()); + return rollbackRecord; + } + + @Override + public List findByProcessInstanceId(Long processInstanceId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + RollbackRecordDAO rollbackRecordDAO = (RollbackRecordDAO) processEngineConfiguration + .getInstanceAccessor().access("rollbackRecordDAO"); + + List entityList = rollbackRecordDAO + .selectByProcessInstanceId(processInstanceId, tenantId); + + List recordList = new ArrayList<>(entityList.size()); + for (RollbackRecordEntity entity : entityList) { + RollbackRecord record = RollbackRecordBuilder.buildFromEntity(entity); + recordList.add(record); + } + + return recordList; + } + + @Override + public RollbackRecord find(Long id, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + RollbackRecordDAO rollbackRecordDAO = (RollbackRecordDAO) processEngineConfiguration + .getInstanceAccessor().access("rollbackRecordDAO"); + + RollbackRecordEntity entity = rollbackRecordDAO.select(id, tenantId); + + if (entity == null) { + return null; + } + + return RollbackRecordBuilder.buildFromEntity(entity); + } + + @Override + public RollbackRecord update(RollbackRecord rollbackRecord, + ProcessEngineConfiguration processEngineConfiguration) { + RollbackRecordDAO rollbackRecordDAO = (RollbackRecordDAO) processEngineConfiguration + .getInstanceAccessor().access("rollbackRecordDAO"); + + RollbackRecordEntity entity = RollbackRecordBuilder.buildEntityFrom(rollbackRecord); + + entity.setGmtModified(DateUtil.getCurrentDate()); + + rollbackRecordDAO.update(entity); + + return rollbackRecord; + } + + @Override + public void remove(Long id, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + RollbackRecordDAO rollbackRecordDAO = (RollbackRecordDAO) processEngineConfiguration + .getInstanceAccessor().access("rollbackRecordDAO"); + + rollbackRecordDAO.delete(id, tenantId); + } +} diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseSupervisionInstanceStorage.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseSupervisionInstanceStorage.java new file mode 100644 index 000000000..810f9e656 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseSupervisionInstanceStorage.java @@ -0,0 +1,152 @@ +package com.alibaba.smart.framework.engine.persister.database.service; + +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.common.util.IdConverter; +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.extension.annotation.ExtensionBinding; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.instance.storage.SupervisionInstanceStorage; +import com.alibaba.smart.framework.engine.model.instance.SupervisionInstance; +import com.alibaba.smart.framework.engine.persister.database.builder.SupervisionInstanceBuilder; +import com.alibaba.smart.framework.engine.persister.database.dao.SupervisionInstanceDAO; +import com.alibaba.smart.framework.engine.persister.database.entity.SupervisionInstanceEntity; +import com.alibaba.smart.framework.engine.service.param.query.SupervisionQueryParam; + +/** + * Supervision instance relational database storage implementation. + * + * @author SmartEngine Team + */ +@ExtensionBinding(group = ExtensionConstant.COMMON, bindKey = SupervisionInstanceStorage.class) +public class RelationshipDatabaseSupervisionInstanceStorage implements SupervisionInstanceStorage { + + @Override + public SupervisionInstance insert(SupervisionInstance supervisionInstance, + ProcessEngineConfiguration processEngineConfiguration) { + SupervisionInstanceDAO supervisionInstanceDAO = getDAO(processEngineConfiguration); + + SupervisionInstanceEntity entity = SupervisionInstanceBuilder + .buildEntityFromSupervisionInstance(supervisionInstance); + + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + + supervisionInstanceDAO.insert(entity); + + // Set generated ID back to instance + supervisionInstance.setInstanceId(IdConverter.toString(entity.getId())); + return supervisionInstance; + } + + @Override + public SupervisionInstance update(SupervisionInstance supervisionInstance, + ProcessEngineConfiguration processEngineConfiguration) { + SupervisionInstanceDAO supervisionInstanceDAO = getDAO(processEngineConfiguration); + + SupervisionInstanceEntity entity = SupervisionInstanceBuilder + .buildEntityFromSupervisionInstance(supervisionInstance); + + entity.setGmtModified(DateUtil.getCurrentDate()); + + supervisionInstanceDAO.update(entity); + + return supervisionInstance; + } + + @Override + public SupervisionInstance find(String supervisionId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + SupervisionInstanceDAO supervisionInstanceDAO = getDAO(processEngineConfiguration); + + Long id = IdConverter.toLong(supervisionId, "supervisionId"); + SupervisionInstanceEntity entity = supervisionInstanceDAO.findOne(id, tenantId); + + return SupervisionInstanceBuilder.buildSupervisionInstanceFromEntity(entity); + } + + @Override + public List findSupervisionList(SupervisionQueryParam param, + ProcessEngineConfiguration processEngineConfiguration) { + SupervisionInstanceDAO supervisionInstanceDAO = getDAO(processEngineConfiguration); + + List entityList = supervisionInstanceDAO.findByQuery(param); + + return buildInstanceList(entityList); + } + + @Override + public Long countSupervision(SupervisionQueryParam param, + ProcessEngineConfiguration processEngineConfiguration) { + SupervisionInstanceDAO supervisionInstanceDAO = getDAO(processEngineConfiguration); + + Integer count = supervisionInstanceDAO.countByQuery(param); + return count != null ? count.longValue() : 0L; + } + + @Override + public List findActiveSupervisionByTask(String taskInstanceId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + SupervisionInstanceDAO supervisionInstanceDAO = getDAO(processEngineConfiguration); + + Long taskId = IdConverter.toLong(taskInstanceId, "taskInstanceId"); + List entityList = supervisionInstanceDAO.findActiveByTaskId(taskId, tenantId); + + return buildInstanceList(entityList); + } + + @Override + public int closeSupervision(String supervisionId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + SupervisionInstanceDAO supervisionInstanceDAO = getDAO(processEngineConfiguration); + + Long id = IdConverter.toLong(supervisionId, "supervisionId"); + return supervisionInstanceDAO.closeSupervision(id, tenantId); + } + + @Override + public int closeSupervisionByTask(String taskInstanceId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + SupervisionInstanceDAO supervisionInstanceDAO = getDAO(processEngineConfiguration); + + Long taskId = IdConverter.toLong(taskInstanceId, "taskInstanceId"); + return supervisionInstanceDAO.closeByTaskId(taskId, tenantId); + } + + @Override + public void remove(String supervisionId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + SupervisionInstanceDAO supervisionInstanceDAO = getDAO(processEngineConfiguration); + + Long id = IdConverter.toLong(supervisionId, "supervisionId"); + supervisionInstanceDAO.delete(id, tenantId); + } + + /** + * Get DAO from configuration. + */ + private SupervisionInstanceDAO getDAO(ProcessEngineConfiguration processEngineConfiguration) { + return (SupervisionInstanceDAO) processEngineConfiguration + .getInstanceAccessor().access("supervisionInstanceDAO"); + } + + /** + * Build instance list from entity list. + */ + private List buildInstanceList(List entityList) { + if (entityList == null || entityList.isEmpty()) { + return new ArrayList<>(); + } + + List instanceList = new ArrayList<>(entityList.size()); + for (SupervisionInstanceEntity entity : entityList) { + SupervisionInstance instance = SupervisionInstanceBuilder.buildSupervisionInstanceFromEntity(entity); + if (instance != null) { + instanceList.add(instance); + } + } + return instanceList; + } +} diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseTaskAssigneeInstanceStorage.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseTaskAssigneeInstanceStorage.java index 9e45a484b..829c80aa6 100644 --- a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseTaskAssigneeInstanceStorage.java +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseTaskAssigneeInstanceStorage.java @@ -3,6 +3,7 @@ import java.util.*; import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.configuration.ConfigurationOption; import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; import com.alibaba.smart.framework.engine.exception.EngineException; import com.alibaba.smart.framework.engine.extension.annotation.ExtensionBinding; @@ -12,7 +13,11 @@ import com.alibaba.smart.framework.engine.model.instance.TaskAssigneeInstance; import com.alibaba.smart.framework.engine.persister.database.builder.TaskAssigneeInstanceBuilder; import com.alibaba.smart.framework.engine.persister.database.dao.TaskAssigneeDAO; +import com.alibaba.smart.framework.engine.persister.database.dao.TaskInstanceDAO; +import com.alibaba.smart.framework.engine.persister.database.dao.UserTaskIndexDAO; import com.alibaba.smart.framework.engine.persister.database.entity.TaskAssigneeEntity; +import com.alibaba.smart.framework.engine.persister.database.entity.TaskInstanceEntity; +import com.alibaba.smart.framework.engine.persister.database.entity.UserTaskIndexEntity; import com.alibaba.smart.framework.engine.service.param.query.PendingTaskQueryParam; import static com.alibaba.smart.framework.engine.persister.common.constant.StorageConstant.NOT_IMPLEMENT_INTENTIONALLY; @@ -100,6 +105,30 @@ public TaskAssigneeInstance insert(TaskAssigneeInstance taskAssigneeInstance, TaskAssigneeInstance resultTaskAssigneeInstance = this.findOne( entityId.toString(), taskAssigneeInstance.getTenantId(), processEngineConfiguration); + if (isShardingEnabled(processEngineConfiguration)) { + TaskInstanceDAO taskInstanceDAO = (TaskInstanceDAO) processEngineConfiguration.getInstanceAccessor().access("taskInstanceDAO"); + TaskInstanceEntity taskEntity = taskInstanceDAO.findOne( + Long.valueOf(taskAssigneeInstance.getTaskInstanceId()), taskAssigneeInstance.getTenantId()); + + if (taskEntity != null) { + UserTaskIndexDAO userTaskIndexDAO = (UserTaskIndexDAO) processEngineConfiguration.getInstanceAccessor().access("userTaskIndexDAO"); + UserTaskIndexEntity indexEntity = new UserTaskIndexEntity(); + indexEntity.setTenantId(taskAssigneeInstance.getTenantId()); + indexEntity.setAssigneeId(taskAssigneeInstance.getAssigneeId()); + indexEntity.setAssigneeType(taskAssigneeInstance.getAssigneeType()); + indexEntity.setTaskInstanceId(Long.valueOf(taskAssigneeInstance.getTaskInstanceId())); + indexEntity.setProcessInstanceId(Long.valueOf(taskAssigneeInstance.getProcessInstanceId())); + indexEntity.setProcessDefinitionType(taskEntity.getProcessDefinitionType()); + indexEntity.setDomainCode(taskEntity.getDomainCode()); + indexEntity.setExtra(taskEntity.getExtra()); + indexEntity.setTaskStatus(taskEntity.getStatus()); + indexEntity.setTaskGmtModified(taskEntity.getGmtModified()); + indexEntity.setTitle(taskEntity.getTitle()); + indexEntity.setPriority(taskEntity.getPriority()); + userTaskIndexDAO.insert(indexEntity); + } + } + return resultTaskAssigneeInstance; } @@ -128,12 +157,30 @@ public TaskAssigneeInstance findOne(String taskAssigneeInstanceId,String tenantI public void remove(String taskAssigneeInstanceId,String tenantId, ProcessEngineConfiguration processEngineConfiguration) { TaskAssigneeDAO taskAssigneeDAO= (TaskAssigneeDAO) processEngineConfiguration.getInstanceAccessor().access("taskAssigneeDAO"); - taskAssigneeDAO.delete(Long.valueOf(taskAssigneeInstanceId),tenantId); + if (isShardingEnabled(processEngineConfiguration)) { + // Find assignee info before deletion + TaskAssigneeEntity entity = taskAssigneeDAO.findOne(Long.valueOf(taskAssigneeInstanceId), tenantId); + taskAssigneeDAO.delete(Long.valueOf(taskAssigneeInstanceId), tenantId); + if (entity != null) { + UserTaskIndexDAO userTaskIndexDAO = (UserTaskIndexDAO) processEngineConfiguration.getInstanceAccessor().access("userTaskIndexDAO"); + userTaskIndexDAO.deleteByAssigneeAndTask(entity.getAssigneeId(), entity.getTaskInstanceId(), tenantId); + } + } else { + taskAssigneeDAO.delete(Long.valueOf(taskAssigneeInstanceId), tenantId); + } } @Override public void removeAll(String taskInstanceId, String tenantId,ProcessEngineConfiguration processEngineConfiguration) { throw new EngineException(NOT_IMPLEMENT_INTENTIONALLY); } + + private boolean isShardingEnabled(ProcessEngineConfiguration config) { + if (config.getOptionContainer() == null) { + return false; + } + ConfigurationOption option = config.getOptionContainer().get("shardingModeEnabled"); + return option != null && option.isEnabled(); + } } diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseTaskInstanceStorage.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseTaskInstanceStorage.java index 942b1e219..fb6e6569c 100644 --- a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseTaskInstanceStorage.java +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseTaskInstanceStorage.java @@ -5,6 +5,7 @@ import java.util.List; import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.configuration.ConfigurationOption; import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; import com.alibaba.smart.framework.engine.constant.TaskInstanceConstant; import com.alibaba.smart.framework.engine.extension.annotation.ExtensionBinding; @@ -14,7 +15,9 @@ import com.alibaba.smart.framework.engine.model.instance.TaskInstance; import com.alibaba.smart.framework.engine.persister.database.builder.TaskInstanceBuilder; import com.alibaba.smart.framework.engine.persister.database.dao.TaskInstanceDAO; +import com.alibaba.smart.framework.engine.persister.database.dao.UserTaskIndexDAO; import com.alibaba.smart.framework.engine.persister.database.entity.TaskInstanceEntity; +import com.alibaba.smart.framework.engine.persister.database.entity.UserTaskIndexEntity; import com.alibaba.smart.framework.engine.service.param.query.PendingTaskQueryParam; import com.alibaba.smart.framework.engine.service.param.query.TaskInstanceQueryByAssigneeParam; import com.alibaba.smart.framework.engine.service.param.query.TaskInstanceQueryParam; @@ -38,6 +41,9 @@ public Long countPendingTaskList(PendingTaskQueryParam pendingTaskQueryParam, @Override public List findTaskListByAssignee(TaskInstanceQueryByAssigneeParam param, ProcessEngineConfiguration processEngineConfiguration) { + if (isShardingEnabled(processEngineConfiguration)) { + return findTaskListByAssigneeFromIndex(param, processEngineConfiguration); + } TaskInstanceDAO taskInstanceDAO= (TaskInstanceDAO) processEngineConfiguration.getInstanceAccessor().access("taskInstanceDAO"); List taskInstanceEntityList= taskInstanceDAO.findTaskByAssignee(param); List taskInstanceList = new ArrayList(taskInstanceEntityList.size()); @@ -54,6 +60,12 @@ public List findTaskListByAssignee(TaskInstanceQueryByAssigneePara @Override public Long countTaskListByAssignee(TaskInstanceQueryByAssigneeParam param, ProcessEngineConfiguration processEngineConfiguration) { + if (isShardingEnabled(processEngineConfiguration)) { + UserTaskIndexDAO userTaskIndexDAO = (UserTaskIndexDAO) processEngineConfiguration + .getInstanceAccessor().access("userTaskIndexDAO"); + Integer count = userTaskIndexDAO.countByAssignee(param); + return count == null ? 0L : count; + } TaskInstanceDAO taskInstanceDAO= (TaskInstanceDAO) processEngineConfiguration.getInstanceAccessor().access("taskInstanceDAO"); Integer count = taskInstanceDAO.countTaskByAssignee(param); return count == null? 0L:count; @@ -63,9 +75,12 @@ public Long countTaskListByAssignee(TaskInstanceQueryByAssigneeParam param, public List findTaskByProcessInstanceIdAndStatus(TaskInstanceQueryParam taskInstanceQueryParam, ProcessEngineConfiguration processEngineConfiguration) { TaskInstanceDAO taskInstanceDAO= (TaskInstanceDAO) processEngineConfiguration.getInstanceAccessor().access("taskInstanceDAO"); - String processInstanceId = taskInstanceQueryParam.getProcessInstanceIdList().get(0); + Object processInstanceIdObj = taskInstanceQueryParam.getProcessInstanceIdList().get(0); + Long processInstanceId = processInstanceIdObj instanceof Long + ? (Long) processInstanceIdObj + : Long.valueOf(processInstanceIdObj.toString()); List taskInstanceEntityList= taskInstanceDAO.findTaskByProcessInstanceIdAndStatus( - Long.valueOf(processInstanceId),taskInstanceQueryParam.getStatus(),taskInstanceQueryParam.getTenantId()); + processInstanceId, taskInstanceQueryParam.getStatus(),taskInstanceQueryParam.getTenantId()); List taskInstanceList = new ArrayList(taskInstanceEntityList.size()); for (TaskInstanceEntity taskInstanceEntity : taskInstanceEntityList) { @@ -154,6 +169,17 @@ public TaskInstance update(TaskInstance taskInstance, TaskInstanceEntity taskInstanceEntity = TaskInstanceBuilder.buildTaskInstanceEntity(taskInstance); taskInstanceDAO.update(taskInstanceEntity); + if (isShardingEnabled(processEngineConfiguration)) { + String status = taskInstance.getStatus(); + if (TaskInstanceConstant.COMPLETED.equals(status) + || TaskInstanceConstant.CANCELED.equals(status) + || TaskInstanceConstant.ABORTED.equals(status)) { + UserTaskIndexDAO userTaskIndexDAO = (UserTaskIndexDAO) processEngineConfiguration + .getInstanceAccessor().access("userTaskIndexDAO"); + userTaskIndexDAO.deleteByTaskInstanceId( + Long.valueOf(taskInstance.getInstanceId()), taskInstance.getTenantId()); + } + } return taskInstance; } @@ -163,7 +189,21 @@ public int updateFromStatus(TaskInstance taskInstance, String fromStatus, ProcessEngineConfiguration processEngineConfiguration) { TaskInstanceDAO taskInstanceDAO= (TaskInstanceDAO) processEngineConfiguration.getInstanceAccessor().access("taskInstanceDAO"); TaskInstanceEntity taskInstanceEntity = TaskInstanceBuilder.buildTaskInstanceEntity(taskInstance); - return taskInstanceDAO.updateFromStatus(taskInstanceEntity,fromStatus); + int result = taskInstanceDAO.updateFromStatus(taskInstanceEntity,fromStatus); + + if (result > 0 && isShardingEnabled(processEngineConfiguration)) { + String status = taskInstance.getStatus(); + if (TaskInstanceConstant.COMPLETED.equals(status) + || TaskInstanceConstant.CANCELED.equals(status) + || TaskInstanceConstant.ABORTED.equals(status)) { + UserTaskIndexDAO userTaskIndexDAO = (UserTaskIndexDAO) processEngineConfiguration + .getInstanceAccessor().access("userTaskIndexDAO"); + userTaskIndexDAO.deleteByTaskInstanceId( + Long.valueOf(taskInstance.getInstanceId()), taskInstance.getTenantId()); + } + } + + return result; } @Override @@ -187,4 +227,42 @@ public void remove(String instanceId,String tenantId, taskInstanceDAO.delete(Long.valueOf(instanceId),tenantId); } + + private List findTaskListByAssigneeFromIndex(TaskInstanceQueryByAssigneeParam param, + ProcessEngineConfiguration processEngineConfiguration) { + UserTaskIndexDAO userTaskIndexDAO = (UserTaskIndexDAO) processEngineConfiguration + .getInstanceAccessor().access("userTaskIndexDAO"); + List indexEntities = userTaskIndexDAO.findByAssignee(param); + + List taskInstanceList = new ArrayList(indexEntities.size()); + for (UserTaskIndexEntity indexEntity : indexEntities) { + TaskInstance taskInstance = buildTaskInstanceFromIndex(indexEntity); + taskInstanceList.add(taskInstance); + } + return taskInstanceList; + } + + private TaskInstance buildTaskInstanceFromIndex(UserTaskIndexEntity indexEntity) { + DefaultTaskInstance taskInstance = new DefaultTaskInstance(); + taskInstance.setInstanceId(indexEntity.getTaskInstanceId().toString()); + taskInstance.setProcessInstanceId(indexEntity.getProcessInstanceId().toString()); + taskInstance.setProcessDefinitionType(indexEntity.getProcessDefinitionType()); + taskInstance.setDomainCode(indexEntity.getDomainCode()); + taskInstance.setExtra(indexEntity.getExtra()); + taskInstance.setStatus(indexEntity.getTaskStatus()); + taskInstance.setTitle(indexEntity.getTitle()); + taskInstance.setPriority(indexEntity.getPriority()); + taskInstance.setTenantId(indexEntity.getTenantId()); + // Note: some fields like activityInstanceId, executionInstanceId are not in the index table. + // Callers that need these fields must query the main table by processInstanceId + taskInstanceId. + return taskInstance; + } + + private boolean isShardingEnabled(ProcessEngineConfiguration config) { + if (config.getOptionContainer() == null) { + return false; + } + ConfigurationOption option = config.getOptionContainer().get("shardingModeEnabled"); + return option != null && option.isEnabled(); + } } diff --git a/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseTaskTransferRecordStorage.java b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseTaskTransferRecordStorage.java new file mode 100644 index 000000000..090b3651c --- /dev/null +++ b/extension/storage/storage-mysql/src/main/java/com/alibaba/smart/framework/engine/persister/database/service/RelationshipDatabaseTaskTransferRecordStorage.java @@ -0,0 +1,97 @@ +package com.alibaba.smart.framework.engine.persister.database.service; + +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.configuration.ProcessEngineConfiguration; +import com.alibaba.smart.framework.engine.extension.annotation.ExtensionBinding; +import com.alibaba.smart.framework.engine.extension.constant.ExtensionConstant; +import com.alibaba.smart.framework.engine.instance.storage.TaskTransferRecordStorage; +import com.alibaba.smart.framework.engine.model.instance.TaskTransferRecord; +import com.alibaba.smart.framework.engine.persister.database.builder.TaskTransferRecordBuilder; +import com.alibaba.smart.framework.engine.persister.database.dao.TaskTransferRecordDAO; +import com.alibaba.smart.framework.engine.persister.database.entity.TaskTransferRecordEntity; + +/** + * 任务移交记录关系数据库存储实现 + * + * @author SmartEngine Team + */ +@ExtensionBinding(group = ExtensionConstant.COMMON, bindKey = TaskTransferRecordStorage.class) +public class RelationshipDatabaseTaskTransferRecordStorage implements TaskTransferRecordStorage { + + @Override + public TaskTransferRecord insert(TaskTransferRecord taskTransferRecord, + ProcessEngineConfiguration processEngineConfiguration) { + TaskTransferRecordDAO taskTransferRecordDAO = (TaskTransferRecordDAO) processEngineConfiguration + .getInstanceAccessor().access("taskTransferRecordDAO"); + + TaskTransferRecordEntity entity = TaskTransferRecordBuilder.buildEntityFrom(taskTransferRecord); + + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + + taskTransferRecordDAO.insert(entity); + + taskTransferRecord.setInstanceId(entity.getId().toString()); + return taskTransferRecord; + } + + @Override + public List findByTaskId(Long taskInstanceId, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + TaskTransferRecordDAO taskTransferRecordDAO = (TaskTransferRecordDAO) processEngineConfiguration + .getInstanceAccessor().access("taskTransferRecordDAO"); + + List entityList = taskTransferRecordDAO + .selectByTaskInstanceId(taskInstanceId, tenantId); + + List recordList = new ArrayList<>(entityList.size()); + for (TaskTransferRecordEntity entity : entityList) { + TaskTransferRecord record = TaskTransferRecordBuilder.buildFromEntity(entity); + recordList.add(record); + } + + return recordList; + } + + @Override + public TaskTransferRecord find(Long id, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + TaskTransferRecordDAO taskTransferRecordDAO = (TaskTransferRecordDAO) processEngineConfiguration + .getInstanceAccessor().access("taskTransferRecordDAO"); + + TaskTransferRecordEntity entity = taskTransferRecordDAO.select(id, tenantId); + + if (entity == null) { + return null; + } + + return TaskTransferRecordBuilder.buildFromEntity(entity); + } + + @Override + public TaskTransferRecord update(TaskTransferRecord taskTransferRecord, + ProcessEngineConfiguration processEngineConfiguration) { + TaskTransferRecordDAO taskTransferRecordDAO = (TaskTransferRecordDAO) processEngineConfiguration + .getInstanceAccessor().access("taskTransferRecordDAO"); + + TaskTransferRecordEntity entity = TaskTransferRecordBuilder.buildEntityFrom(taskTransferRecord); + + entity.setGmtModified(DateUtil.getCurrentDate()); + + taskTransferRecordDAO.update(entity); + + return taskTransferRecord; + } + + @Override + public void remove(Long id, String tenantId, + ProcessEngineConfiguration processEngineConfiguration) { + TaskTransferRecordDAO taskTransferRecordDAO = (TaskTransferRecordDAO) processEngineConfiguration + .getInstanceAccessor().access("taskTransferRecordDAO"); + + taskTransferRecordDAO.delete(id, tenantId); + } +} diff --git a/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/activity_instance.xml b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/activity_instance.xml index aa8f6764b..6982c2890 100644 --- a/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/activity_instance.xml +++ b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/activity_instance.xml @@ -26,7 +26,7 @@ + + + + ORDER BY + + ${spec.columnName} ${spec.directionSql} + + + + ORDER BY gmt_modified DESC + + + diff --git a/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/execution_instance.xml b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/execution_instance.xml index 2c59c1401..de4d48897 100644 --- a/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/execution_instance.xml +++ b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/execution_instance.xml @@ -16,7 +16,12 @@ insert into se_execution_instance() values (#{id}, #{gmtCreate}, #{gmtModified}, #{processInstanceId}, #{processDefinitionIdAndVersion}, #{processDefinitionActivityId}, - #{activityInstanceId},#{active},#{blockId},#{tenantId}) + #{activityInstanceId}, + CASE + WHEN #{active} THEN 1 + ELSE 0 + END, + #{blockId},#{tenantId}) @@ -30,8 +35,14 @@ parameterType="com.alibaba.smart.framework.engine.persister.database.entity.ExecutionInstanceEntity"> update se_execution_instance - gmt_modified=now(6) - ,active = #{active} + gmt_modified=CURRENT_TIMESTAMP + + ,active = + CASE + WHEN #{active} THEN 1 + ELSE 0 + END + where id=#{id} and tenant_id = #{tenantId} diff --git a/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/notification_instance.xml b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/notification_instance.xml new file mode 100644 index 000000000..359e1b493 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/notification_instance.xml @@ -0,0 +1,159 @@ + + + + + + id, gmt_create, gmt_modified, process_instance_id, task_instance_id, + sender_user_id, receiver_user_id, notification_type, title, content, + read_status, read_time, tenant_id + + + + + and tenant_id = #{tenantId} + and sender_user_id = #{senderUserId} + and receiver_user_id = #{receiverUserId} + and read_status = #{readStatus} + and notification_type = #{notificationType} + + and task_instance_id in + + #{item, typeHandler=com.alibaba.smart.framework.engine.persister.database.handler.SmartIdTypeHandler} + + + + and process_instance_id in + + #{item, typeHandler=com.alibaba.smart.framework.engine.persister.database.handler.SmartIdTypeHandler} + + + and gmt_create =]]> #{notificationStartTime} + and gmt_create #{notificationEndTime} + + + + + insert into se_notification_instance() + values (#{id}, #{gmtCreate}, #{gmtModified}, #{processInstanceId}, #{taskInstanceId}, + #{senderUserId}, #{receiverUserId}, #{notificationType}, #{title}, #{content}, + #{readStatus}, #{readTime}, #{tenantId}) + + + + update se_notification_instance + + gmt_modified = CURRENT_TIMESTAMP + ,title = #{notificationInstanceEntity.title} + ,content = #{notificationInstanceEntity.content} + ,read_status = #{notificationInstanceEntity.readStatus} + ,read_time = #{notificationInstanceEntity.readTime} + + where id = #{notificationInstanceEntity.id} + and tenant_id = #{notificationInstanceEntity.tenantId} + + + + + + + + + + + + + + update se_notification_instance + set read_status = 'read', + read_time = CURRENT_TIMESTAMP, + gmt_modified = CURRENT_TIMESTAMP + where id = #{id} + and tenant_id = #{tenantId} + + + + update se_notification_instance + set read_status = 'read', + read_time = CURRENT_TIMESTAMP, + gmt_modified = CURRENT_TIMESTAMP + where id in + + #{item} + + and tenant_id = #{tenantId} + + + + delete from se_notification_instance + where id = #{id} + and tenant_id = #{tenantId} + + + + + + + + + + + ORDER BY + + ${spec.columnName} ${spec.directionSql} + + + + ORDER BY gmt_create DESC + + + + + \ No newline at end of file diff --git a/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/process_instance.xml b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/process_instance.xml index 95885995a..b54620cb2 100644 --- a/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/process_instance.xml +++ b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/process_instance.xml @@ -5,7 +5,7 @@ id, gmt_create, gmt_modified, process_definition_id_and_version,process_definition_type,start_user_id, - status, parent_process_instance_id,parent_execution_instance_id, reason, biz_unique_id, title,tag,comment,tenant_id + status, complete_time, parent_process_instance_id,parent_execution_instance_id, reason, biz_unique_id, title,tag,comment,tenant_id ) values (#{id}, #{gmtCreate}, #{gmtModified}, #{processDefinitionIdAndVersion},#{processDefinitionType},#{startUserId}, - #{status}, #{parentProcessInstanceId},#{parentExecutionInstanceId}, #{reason}, #{bizUniqueId}, #{title}, + #{status}, #{completeTime}, #{parentProcessInstanceId},#{parentExecutionInstanceId}, #{reason}, #{bizUniqueId}, #{title}, #{tag},#{comment}, #{tenantId}) @@ -31,8 +31,9 @@ parameterType="com.alibaba.smart.framework.engine.persister.database.entity.ProcessInstanceEntity"> update se_process_instance - gmt_modified=now(6) + gmt_modified=CURRENT_TIMESTAMP ,status = #{status} + ,complete_time = #{completeTime} ,parent_process_instance_id = #{parentProcessInstanceId} ,reason = #{reason} ,tag = #{tag} @@ -80,8 +81,8 @@ from se_process_instance - order by gmt_modified desc - limit #{pageOffset},#{pageSize} + + limit #{pageSize} offset #{pageOffset} @@ -102,18 +103,41 @@ and status = #{status} and start_user_id = #{startUserId} - and parent_process_instance_id = #{parentInstanceId} + and parent_process_instance_id = #{parentInstanceId, typeHandler=com.alibaba.smart.framework.engine.persister.database.handler.SmartIdTypeHandler} and biz_unique_id = #{bizUniqueId} and gmt_create =]]> #{processStartTime} and gmt_create #{processEndTime} + and complete_time =]]> #{completeTimeStart} + and complete_time #{completeTimeEnd} and id in + #{item, typeHandler=com.alibaba.smart.framework.engine.persister.database.handler.SmartIdTypeHandler} + + + + and process_definition_type in + #{item} + + + + + ORDER BY + + ${spec.columnName} ${spec.directionSql} + + + + ORDER BY gmt_modified DESC + + + + diff --git a/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/process_rollback_record.xml b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/process_rollback_record.xml new file mode 100644 index 000000000..caae7201a --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/process_rollback_record.xml @@ -0,0 +1,51 @@ + + + + + + id, gmt_create, gmt_modified, process_instance_id, task_instance_id, + rollback_type, from_activity_id, to_activity_id, operator_user_id, + rollback_reason, tenant_id + + + + insert into se_process_rollback_record() + values (#{id}, #{gmtCreate}, #{gmtModified}, #{processInstanceId}, #{taskInstanceId}, + #{rollbackType}, #{fromActivityId}, #{toActivityId}, #{operatorUserId}, + #{rollbackReason}, #{tenantId}) + + + + update se_process_rollback_record + + gmt_modified = CURRENT_TIMESTAMP + ,rollback_reason = #{rollbackReason} + + where id = #{id} + and tenant_id = #{tenantId} + + + + + + + + delete from se_process_rollback_record + where id = #{id} + and tenant_id = #{tenantId} + + + diff --git a/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/supervision_instance.xml b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/supervision_instance.xml new file mode 100644 index 000000000..fb24cb3db --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/supervision_instance.xml @@ -0,0 +1,145 @@ + + + + + + id, gmt_create, gmt_modified, process_instance_id, task_instance_id, + supervisor_user_id, supervision_reason, supervision_type, status, close_time, tenant_id + + + + + and tenant_id = #{tenantId} + and supervisor_user_id = #{supervisorUserId} + and status = #{status} + and supervision_type = #{supervisionType} + + and task_instance_id in + + #{item, typeHandler=com.alibaba.smart.framework.engine.persister.database.handler.SmartIdTypeHandler} + + + + and process_instance_id in + + #{item, typeHandler=com.alibaba.smart.framework.engine.persister.database.handler.SmartIdTypeHandler} + + + and gmt_create =]]> #{supervisionStartTime} + and gmt_create #{supervisionEndTime} + + + + + insert into se_supervision_instance() + values (#{id}, #{gmtCreate}, #{gmtModified}, #{processInstanceId}, #{taskInstanceId}, + #{supervisorUserId}, #{supervisionReason}, #{supervisionType}, #{status}, #{closeTime}, #{tenantId}) + + + + update se_supervision_instance + + gmt_modified = CURRENT_TIMESTAMP + ,supervision_reason = #{supervisionInstanceEntity.supervisionReason} + ,supervision_type = #{supervisionInstanceEntity.supervisionType} + ,status = #{supervisionInstanceEntity.status} + ,close_time = #{supervisionInstanceEntity.closeTime} + + where id = #{supervisionInstanceEntity.id} + and tenant_id = #{supervisionInstanceEntity.tenantId} + + + + + + + + + + + + update se_supervision_instance + set status = 'closed', + close_time = CURRENT_TIMESTAMP, + gmt_modified = CURRENT_TIMESTAMP + where id = #{id} + and tenant_id = #{tenantId} + + + + update se_supervision_instance + set status = 'closed', + close_time = CURRENT_TIMESTAMP, + gmt_modified = CURRENT_TIMESTAMP + where task_instance_id = #{taskInstanceId} + and status = 'active' + and tenant_id = #{tenantId} + + + + delete from se_supervision_instance + where id = #{id} + and tenant_id = #{tenantId} + + + + + + + + + + + ORDER BY + + ${spec.columnName} ${spec.directionSql} + + + + ORDER BY gmt_create DESC + + + + + \ No newline at end of file diff --git a/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/task_assignee_instance.xml b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/task_assignee_instance.xml index 2af3f2726..933287542 100644 --- a/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/task_assignee_instance.xml +++ b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/task_assignee_instance.xml @@ -26,7 +26,7 @@ update se_task_assignee_instance - gmt_modified=now(6) + gmt_modified=CURRENT_TIMESTAMP ,assignee_id = #{assigneeId} diff --git a/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/task_instance.xml b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/task_instance.xml index 90d478814..addd40746 100644 --- a/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/task_instance.xml +++ b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/task_instance.xml @@ -5,7 +5,7 @@ id, gmt_create, gmt_modified,process_instance_id, process_definition_id_and_version,process_definition_type, - activity_instance_id, process_definition_activity_id, execution_instance_id,claim_user_id,priority,tag,claim_time,complete_time,status,comment,extension,title,tenant_id + activity_instance_id, process_definition_activity_id, execution_instance_id,claim_user_id,priority,tag,claim_time,complete_time,status,comment,extension,domain_code,extra,title,tenant_id @@ -15,7 +15,7 @@ values (#{id}, #{gmtCreate}, #{gmtModified}, #{processInstanceId}, #{processDefinitionIdAndVersion},#{processDefinitionType}, #{activityInstanceId}, - #{processDefinitionActivityId},#{executionInstanceId},#{claimUserId},#{priority},#{tag},#{claimTime},#{completeTime},#{status},#{comment},#{extension},#{title}, + #{processDefinitionActivityId},#{executionInstanceId},#{claimUserId},#{priority},#{tag},#{claimTime},#{completeTime},#{status},#{comment},#{extension},#{domainCode},#{extra, typeHandler=com.alibaba.smart.framework.engine.persister.database.handler.JsonTypeHandler},#{title}, #{tenantId}) @@ -28,7 +28,7 @@ - gmt_modified=now(6) + gmt_modified=CURRENT_TIMESTAMP ,claim_user_id = #{taskInstanceEntity.claimUserId} ,priority = #{taskInstanceEntity.priority} ,claim_time = #{taskInstanceEntity.claimTime} @@ -37,6 +37,8 @@ , tag = #{taskInstanceEntity.tag} , comment = #{taskInstanceEntity.comment} , extension = #{taskInstanceEntity.extension} + , domain_code = #{taskInstanceEntity.domainCode} + , extra = #{taskInstanceEntity.extra, typeHandler=com.alibaba.smart.framework.engine.persister.database.handler.JsonTypeHandler} , title = #{taskInstanceEntity.title} @@ -80,7 +82,7 @@ select distinct task.id, task.gmt_create, task.gmt_modified,task.process_instance_id,task.tenant_id, process_definition_id_and_version, activity_instance_id,process_definition_activity_id, execution_instance_id,claim_user_id,priority, task.tag, - task.process_definition_type, claim_time,complete_time,status,comment,extension,title + task.process_definition_type, claim_time,complete_time,status,comment,extension,task.domain_code,task.extra,title from se_task_instance task,se_task_assignee_instance assignee where task.id = assignee.task_instance_id @@ -96,7 +98,7 @@ order by task.gmt_modified desc - limit #{pageOffset},#{pageSize} + limit #{pageSize} offset #{pageOffset} @@ -117,7 +119,7 @@ - limit #{pageOffset},#{pageSize} + limit #{pageSize} offset #{pageOffset} @@ -152,12 +154,41 @@ and task.process_definition_type = #{processDefinitionType} + and task.process_instance_id in #{item} + + and task.domain_code = #{domainCode} + and task.domain_code like CONCAT('%', #{domainCodeLike}, '%') + + and task.domain_code in + + #{item} + + + + + + and ${jc.sqlExpression} = #{jc.value} + + + + + and ${jic.sqlExpression} IN + + #{v} + + + + + + and ${jlc.sqlExpression} LIKE CONCAT('%', #{jlc.value}, '%') + + @@ -166,14 +197,14 @@ parameterType="com.alibaba.smart.framework.engine.service.param.query.TaskInstanceQueryParam"> select task.id, task.gmt_create, task.gmt_modified,task.process_instance_id,task.tenant_id, process_definition_id_and_version, activity_instance_id,process_definition_activity_id, execution_instance_id,claim_user_id,priority, task.tag, - task.process_definition_type, claim_time,complete_time,status,comment,extension,title + task.process_definition_type, claim_time,complete_time,status,comment,extension,task.domain_code,task.extra,title from se_task_instance task where 1=1 and tenant_id = #{tenantId} - order by task.gmt_modified desc - limit #{pageOffset},#{pageSize} + + limit #{pageSize} offset #{pageOffset} @@ -186,7 +217,7 @@ - limit #{pageOffset},#{pageSize} + limit #{pageSize} offset #{pageOffset} @@ -195,23 +226,87 @@ and task.status = #{status} and task.process_definition_type = #{processDefinitionType} and task.claim_user_id = #{claimUserId} + and task.claim_user_id like CONCAT('%', #{claimUserIdLike}, '%') + and task.claim_user_id is null and task.tag = #{tag} and task.extension = #{extension} and task.title = #{title} + and task.title like CONCAT('%', #{titleLike}, '%') and task.priority = #{priority} + and task.priority =]]> #{minPriority} + and task.priority #{maxPriority} and task.comment = #{comment} - and task.activity_instance_id = #{activityInstanceId} + and task.activity_instance_id = #{activityInstanceId, typeHandler=com.alibaba.smart.framework.engine.persister.database.handler.SmartIdTypeHandler} and task.process_definition_activity_id = #{processDefinitionActivityId} + and task.complete_time =]]> #{completeTimeStart} + and task.complete_time #{completeTimeEnd} + and task.gmt_create =]]> #{createTimeStart} + and task.gmt_create #{createTimeEnd} and task.process_instance_id in + #{item, typeHandler=com.alibaba.smart.framework.engine.persister.database.handler.SmartIdTypeHandler} + + + + and task.status in + + #{item} + + + + and task.process_definition_type in + + #{item} + + + + and task.domain_code = #{domainCode} + and task.domain_code like CONCAT('%', #{domainCodeLike}, '%') + + and task.domain_code in + #{item} + + + + and ${jc.sqlExpression} = #{jc.value} + + + + + and ${jic.sqlExpression} IN + + #{v} + + + + + + and ${jlc.sqlExpression} LIKE CONCAT('%', #{jlc.value}, '%') + + + + + + + + + ORDER BY + + task.${spec.columnName} ${spec.directionSql} + + + + ORDER BY task.gmt_modified DESC + + diff --git a/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/task_transfer_record.xml b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/task_transfer_record.xml new file mode 100644 index 000000000..575959463 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/task_transfer_record.xml @@ -0,0 +1,50 @@ + + + + + + id, gmt_create, gmt_modified, task_instance_id, process_instance_id, from_user_id, to_user_id, + transfer_reason, deadline, tenant_id + + + + insert into se_task_transfer_record() + values (#{id}, #{gmtCreate}, #{gmtModified}, #{taskInstanceId}, #{processInstanceId}, #{fromUserId}, + #{toUserId}, #{transferReason}, #{deadline}, #{tenantId}) + + + + update se_task_transfer_record + + gmt_modified = CURRENT_TIMESTAMP + ,transfer_reason = #{transferReason} + ,deadline = #{deadline} + + where id = #{id} + and tenant_id = #{tenantId} + + + + + + + + delete from se_task_transfer_record + where id = #{id} + and tenant_id = #{tenantId} + + + diff --git a/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/user_notification_index.xml b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/user_notification_index.xml new file mode 100644 index 000000000..39c46dfc2 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/user_notification_index.xml @@ -0,0 +1,56 @@ + + + + + + id, tenant_id, receiver_user_id, notification_id, process_instance_id, + notification_type, title, read_status, gmt_create + + + + insert into se_user_notification_index(tenant_id, receiver_user_id, notification_id, process_instance_id, + notification_type, title, read_status, gmt_create) + values (#{tenantId}, #{receiverUserId}, #{notificationId}, #{processInstanceId}, + #{notificationType}, #{title}, #{readStatus}, #{gmtCreate}) + + + + delete from se_user_notification_index where notification_id = #{notificationId} + and tenant_id = #{tenantId} + + + + update se_user_notification_index set read_status = #{readStatus} + where notification_id = #{notificationId} + and tenant_id = #{tenantId} + + + + update se_user_notification_index set read_status = #{readStatus} + where notification_id in + + #{item} + + and tenant_id = #{tenantId} + + + + + + + diff --git a/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/user_task_index.xml b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/user_task_index.xml new file mode 100644 index 000000000..bb116a4c1 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/user_task_index.xml @@ -0,0 +1,139 @@ + + + + + + id, tenant_id, assignee_id, assignee_type, task_instance_id, process_instance_id, + process_definition_type, domain_code, extra, task_status, task_gmt_modified, title, priority + + + + insert into se_user_task_index(tenant_id, assignee_id, assignee_type, task_instance_id, process_instance_id, + process_definition_type, domain_code, extra, task_status, task_gmt_modified, title, priority) + values (#{tenantId}, #{assigneeId}, #{assigneeType}, #{taskInstanceId}, #{processInstanceId}, + #{processDefinitionType}, #{domainCode}, #{extra, typeHandler=com.alibaba.smart.framework.engine.persister.database.handler.JsonTypeHandler}, #{taskStatus}, #{taskGmtModified}, #{title}, #{priority}) + + + + delete from se_user_task_index where task_instance_id = #{taskInstanceId} + and tenant_id = #{tenantId} + + + + delete from se_user_task_index where assignee_id = #{assigneeId} and task_instance_id = #{taskInstanceId} + and tenant_id = #{tenantId} + + + + update se_user_task_index + + task_status = #{taskStatus} + + where task_instance_id = #{taskInstanceId} + and tenant_id = #{tenantId} + + + + update se_user_task_index + + title = #{title}, + priority = #{priority}, + domain_code = #{domainCode}, + + where task_instance_id = #{taskInstanceId} + and tenant_id = #{tenantId} + + + + + + and ( (assignee_id = #{assigneeUserId} and assignee_type = 'user') or + (assignee_id in + + #{item} + + and assignee_type = 'group') ) + + + and assignee_id = #{assigneeUserId} and assignee_type = 'user' + + + and assignee_id in + + #{item} + + and assignee_type = 'group' + + + + + + + + and process_definition_type = #{processDefinitionType} + + and process_instance_id in + + #{item} + + + and domain_code = #{domainCode} + and domain_code like CONCAT('%', #{domainCodeLike}, '%') + + and domain_code in + + #{item} + + + + + + and ${jc.sqlExpression} = #{jc.value} + + + + + and ${jic.sqlExpression} IN + + #{v} + + + + + + and ${jlc.sqlExpression} LIKE CONCAT('%', #{jlc.value}, '%') + + + + + + + + + diff --git a/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/variable_instance.xml b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/variable_instance.xml index c906f6e53..f5edb0bfd 100644 --- a/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/variable_instance.xml +++ b/extension/storage/storage-mysql/src/main/resources/mybatis/sqlmap/variable_instance.xml @@ -28,7 +28,7 @@ parameterType="com.alibaba.smart.framework.engine.persister.database.entity.VariableInstanceEntity"> update se_variable_instance - gmt_modified=now(6) + gmt_modified=CURRENT_TIMESTAMP ,field_long_value = #{fieldLongValue} ,field_double_value = #{fieldDoubleValue} ,field_string_value = #{fieldStringValue} diff --git a/extension/storage/storage-mysql/src/main/resources/sql/index-oracle.sql b/extension/storage/storage-mysql/src/main/resources/sql/index-oracle.sql new file mode 100644 index 000000000..e6451645e --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/index-oracle.sql @@ -0,0 +1,70 @@ +-- Oracle/DM (DaMeng) index definitions for SmartEngine +-- Compatible with both Oracle Database and DaMeng Database +-- Includes indexes from both base tables and workflow enhancement tables + +-- =========================================== +-- Base table indexes +-- =========================================== + +-- se_execution_instance indexes +CREATE INDEX idx_exec_tenant_proc_active ON se_execution_instance (tenant_id, process_instance_id, active); +CREATE INDEX idx_exec_tenant_proc_act ON se_execution_instance (tenant_id, process_instance_id, activity_instance_id); + +-- se_task_assignee_instance indexes +CREATE INDEX idx_assignee_tenant_task ON se_task_assignee_instance (tenant_id, task_instance_id); +CREATE INDEX idx_assignee_tenant_id_type ON se_task_assignee_instance (tenant_id, assignee_id, assignee_type); + +-- se_activity_instance indexes +CREATE INDEX idx_activity_tenant_proc ON se_activity_instance (tenant_id, process_instance_id); + +-- se_deployment_instance indexes +CREATE INDEX idx_deploy_tenant_logic_user ON se_deployment_instance (tenant_id, logic_status, deployment_user_id); + +-- se_process_instance indexes +CREATE INDEX idx_proc_tenant_start_user ON se_process_instance (tenant_id, start_user_id); +CREATE INDEX idx_proc_tenant_status ON se_process_instance (tenant_id, status); + +-- se_task_instance indexes +CREATE INDEX idx_task_tenant_status ON se_task_instance (tenant_id, status); +CREATE INDEX idx_task_tenant_proc_status ON se_task_instance (tenant_id, process_instance_id, status); +CREATE INDEX idx_task_tenant_def_type ON se_task_instance (tenant_id, process_definition_type); +CREATE INDEX idx_task_tenant_proc ON se_task_instance (tenant_id, process_instance_id); +CREATE INDEX idx_task_tenant_claim_user ON se_task_instance (tenant_id, claim_user_id); +CREATE INDEX idx_task_tenant_tag ON se_task_instance (tenant_id, tag); +CREATE INDEX idx_task_tenant_act_inst ON se_task_instance (tenant_id, activity_instance_id); +CREATE INDEX idx_task_tenant_def_act_id ON se_task_instance (tenant_id, process_definition_activity_id); + +-- se_variable_instance indexes +CREATE INDEX idx_var_tenant_proc_exec ON se_variable_instance (tenant_id, process_instance_id, execution_instance_id); + +-- =========================================== +-- Workflow enhancement table indexes +-- =========================================== + +-- se_supervision_instance indexes +CREATE INDEX idx_supervision_task_status_tenant ON se_supervision_instance (task_instance_id, status, tenant_id); +CREATE INDEX idx_supervision_supervisor_tenant ON se_supervision_instance (supervisor_user_id, tenant_id); +CREATE INDEX idx_supervision_process_instance_id ON se_supervision_instance (process_instance_id); + +-- se_notification_instance indexes +CREATE INDEX idx_notification_receiver_read_tenant ON se_notification_instance (receiver_user_id, read_status, tenant_id); +CREATE INDEX idx_notification_sender_tenant ON se_notification_instance (sender_user_id, tenant_id); +CREATE INDEX idx_notification_process_instance_id ON se_notification_instance (process_instance_id); +CREATE INDEX idx_notification_task_instance_id ON se_notification_instance (task_instance_id); + +-- se_task_transfer_record indexes +CREATE INDEX idx_task_transfer_task_tenant ON se_task_transfer_record (task_instance_id, tenant_id); +CREATE INDEX idx_task_transfer_from_user_id ON se_task_transfer_record (from_user_id); +CREATE INDEX idx_task_transfer_to_user_id ON se_task_transfer_record (to_user_id); + +-- se_assignee_operation_record indexes +CREATE INDEX idx_assignee_op_task_tenant ON se_assignee_operation_record (task_instance_id, tenant_id); +CREATE INDEX idx_assignee_op_operation_type ON se_assignee_operation_record (operation_type); +CREATE INDEX idx_assignee_op_operator_user_id ON se_assignee_operation_record (operator_user_id); +CREATE INDEX idx_assignee_op_target_user_id ON se_assignee_operation_record (target_user_id); + +-- se_process_rollback_record indexes +CREATE INDEX idx_rollback_process_tenant ON se_process_rollback_record (process_instance_id, tenant_id); +CREATE INDEX idx_rollback_task_instance_id ON se_process_rollback_record (task_instance_id); +CREATE INDEX idx_rollback_type ON se_process_rollback_record (rollback_type); +CREATE INDEX idx_rollback_operator_user_id ON se_process_rollback_record (operator_user_id); diff --git a/extension/storage/storage-mysql/src/main/resources/sql/migration/V001__add_process_complete_time.sql b/extension/storage/storage-mysql/src/main/resources/sql/migration/V001__add_process_complete_time.sql new file mode 100644 index 000000000..03a7a08ef --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/migration/V001__add_process_complete_time.sql @@ -0,0 +1,11 @@ +-- Migration: Add complete_time field to se_process_instance table +-- Purpose: Track when processes are completed for accurate completed process queries +-- Date: 2026-01-08 + +-- PostgreSQL compatible syntax +ALTER TABLE se_process_instance +ADD COLUMN complete_time timestamp(6) DEFAULT NULL; + +COMMENT ON COLUMN se_process_instance.complete_time IS 'process completion time'; + +CREATE INDEX idx_complete_time ON se_process_instance(complete_time); diff --git a/extension/storage/storage-mysql/src/main/resources/sql/migration/V001__add_process_complete_time_oracle.sql b/extension/storage/storage-mysql/src/main/resources/sql/migration/V001__add_process_complete_time_oracle.sql new file mode 100644 index 000000000..5ec5d4cf6 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/migration/V001__add_process_complete_time_oracle.sql @@ -0,0 +1,10 @@ +-- Migration: Add complete_time field to se_process_instance table +-- Oracle/DM (DaMeng) compatible syntax +-- Purpose: Track when processes are completed for accurate completed process queries +-- Date: 2026-01-08 + +ALTER TABLE se_process_instance ADD complete_time TIMESTAMP(6) DEFAULT NULL; + +COMMENT ON COLUMN se_process_instance.complete_time IS 'process completion time'; + +CREATE INDEX idx_complete_time ON se_process_instance (complete_time); diff --git a/extension/storage/storage-mysql/src/main/resources/sql/migration/V002__add_domain_code_and_extra.sql b/extension/storage/storage-mysql/src/main/resources/sql/migration/V002__add_domain_code_and_extra.sql new file mode 100644 index 000000000..047d20ce3 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/migration/V002__add_domain_code_and_extra.sql @@ -0,0 +1,6 @@ +-- Add domain_code and extra columns to se_task_instance (MySQL) +ALTER TABLE se_task_instance + ADD COLUMN domain_code varchar(64) DEFAULT NULL COMMENT 'domain code', + ADD COLUMN extra json DEFAULT NULL COMMENT 'extra JSON data'; + +CREATE INDEX idx_task_domain_code ON se_task_instance(domain_code); diff --git a/extension/storage/storage-mysql/src/main/resources/sql/migration/V002__add_domain_code_and_extra_oracle.sql b/extension/storage/storage-mysql/src/main/resources/sql/migration/V002__add_domain_code_and_extra_oracle.sql new file mode 100644 index 000000000..2c72db1bf --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/migration/V002__add_domain_code_and_extra_oracle.sql @@ -0,0 +1,5 @@ +-- Add domain_code and extra columns to se_task_instance (Oracle/DM) +ALTER TABLE se_task_instance ADD domain_code VARCHAR2(64) DEFAULT NULL; +ALTER TABLE se_task_instance ADD extra CLOB DEFAULT NULL CHECK (extra IS JSON); + +CREATE INDEX idx_task_domain_code ON se_task_instance(domain_code); diff --git a/extension/storage/storage-mysql/src/main/resources/sql/migration/V002__add_domain_code_and_extra_postgresql.sql b/extension/storage/storage-mysql/src/main/resources/sql/migration/V002__add_domain_code_and_extra_postgresql.sql new file mode 100644 index 000000000..8f909ed38 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/migration/V002__add_domain_code_and_extra_postgresql.sql @@ -0,0 +1,7 @@ +-- Add domain_code and extra columns to se_task_instance (PostgreSQL) +ALTER TABLE se_task_instance + ADD COLUMN domain_code varchar(64) DEFAULT NULL, + ADD COLUMN extra jsonb DEFAULT NULL; + +CREATE INDEX idx_task_domain_code ON se_task_instance(domain_code); +CREATE INDEX idx_task_extra_gin ON se_task_instance USING GIN (extra); diff --git a/extension/storage/storage-mysql/src/main/resources/sql/migration/V1.1__optimize_indexes.sql b/extension/storage/storage-mysql/src/main/resources/sql/migration/V1.1__optimize_indexes.sql new file mode 100644 index 000000000..7b72f59f3 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/migration/V1.1__optimize_indexes.sql @@ -0,0 +1,70 @@ +-- =========================================== +-- Index optimization migration script +-- Version: 1.1 +-- Description: Optimize indexes for better query performance +-- =========================================== + +-- =========================================== +-- MySQL Version +-- =========================================== + +-- Step 1: Add new composite indexes (before dropping old ones) + +-- se_supervision_instance: Composite index for findActiveByTaskId +ALTER TABLE `se_supervision_instance` +ADD KEY `idx_task_status_tenant` (`task_instance_id`, `status`, `tenant_id`); + +-- se_supervision_instance: Composite index for findBySupervisor +ALTER TABLE `se_supervision_instance` +ADD KEY `idx_supervisor_tenant` (`supervisor_user_id`, `tenant_id`); + +-- se_notification_instance: Composite index for findByReceiver (most important) +ALTER TABLE `se_notification_instance` +ADD KEY `idx_receiver_read_tenant` (`receiver_user_id`, `read_status`, `tenant_id`); + +-- se_notification_instance: Composite index for findBySender +ALTER TABLE `se_notification_instance` +ADD KEY `idx_sender_tenant` (`sender_user_id`, `tenant_id`); + +-- se_task_transfer_record: Composite index +ALTER TABLE `se_task_transfer_record` +ADD KEY `idx_task_tenant` (`task_instance_id`, `tenant_id`); + +-- se_assignee_operation_record: Composite index +ALTER TABLE `se_assignee_operation_record` +ADD KEY `idx_task_tenant` (`task_instance_id`, `tenant_id`); + +-- se_process_rollback_record: Composite index +ALTER TABLE `se_process_rollback_record` +ADD KEY `idx_process_tenant` (`process_instance_id`, `tenant_id`); + +-- Step 2: Drop inefficient indexes + +-- Drop low-selectivity single-column indexes (status only has 2 values) +ALTER TABLE `se_supervision_instance` DROP KEY `idx_status`; +ALTER TABLE `se_notification_instance` DROP KEY `idx_read_status`; + +-- Drop redundant tenant_id indexes (replaced by composite indexes) +ALTER TABLE `se_supervision_instance` DROP KEY `idx_tenant_id`; +ALTER TABLE `se_notification_instance` DROP KEY `idx_tenant_id`; +ALTER TABLE `se_task_transfer_record` DROP KEY `idx_tenant_id`; +ALTER TABLE `se_assignee_operation_record` DROP KEY `idx_tenant_id`; +ALTER TABLE `se_process_rollback_record` DROP KEY `idx_tenant_id`; + +-- Drop single-column indexes replaced by composite indexes +ALTER TABLE `se_supervision_instance` DROP KEY `idx_task_instance_id`; +ALTER TABLE `se_supervision_instance` DROP KEY `idx_supervisor_user_id`; +ALTER TABLE `se_notification_instance` DROP KEY `idx_receiver_user_id`; +ALTER TABLE `se_notification_instance` DROP KEY `idx_sender_user_id`; +ALTER TABLE `se_task_transfer_record` DROP KEY `idx_task_instance_id`; +ALTER TABLE `se_assignee_operation_record` DROP KEY `idx_task_instance_id`; +ALTER TABLE `se_process_rollback_record` DROP KEY `idx_process_instance_id`; + +-- =========================================== +-- Verification queries +-- =========================================== +-- SHOW INDEX FROM `se_supervision_instance`; +-- SHOW INDEX FROM `se_notification_instance`; +-- SHOW INDEX FROM `se_task_transfer_record`; +-- SHOW INDEX FROM `se_assignee_operation_record`; +-- SHOW INDEX FROM `se_process_rollback_record`; diff --git a/extension/storage/storage-mysql/src/main/resources/sql/migration/V1.1__optimize_indexes_postgresql.sql b/extension/storage/storage-mysql/src/main/resources/sql/migration/V1.1__optimize_indexes_postgresql.sql new file mode 100644 index 000000000..f9a019620 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/migration/V1.1__optimize_indexes_postgresql.sql @@ -0,0 +1,78 @@ +-- =========================================== +-- Index optimization migration script for PostgreSQL +-- Version: 1.1 +-- Description: Optimize indexes for better query performance +-- =========================================== + +-- Step 1: Add new composite indexes (before dropping old ones) + +-- se_supervision_instance: Composite index for findActiveByTaskId +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_supervision_task_status_tenant + ON se_supervision_instance (task_instance_id, status, tenant_id); + +-- se_supervision_instance: Composite index for findBySupervisor +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_supervision_supervisor_tenant + ON se_supervision_instance (supervisor_user_id, tenant_id); + +-- se_notification_instance: Composite index for findByReceiver (most important) +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_notification_receiver_read_tenant + ON se_notification_instance (receiver_user_id, read_status, tenant_id); + +-- se_notification_instance: Composite index for findBySender +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_notification_sender_tenant + ON se_notification_instance (sender_user_id, tenant_id); + +-- se_task_transfer_record: Composite index +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_task_transfer_task_tenant + ON se_task_transfer_record (task_instance_id, tenant_id); + +-- se_assignee_operation_record: Composite index +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_assignee_op_task_tenant + ON se_assignee_operation_record (task_instance_id, tenant_id); + +-- se_process_rollback_record: Composite index +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_rollback_process_tenant + ON se_process_rollback_record (process_instance_id, tenant_id); + +-- Step 2: Drop inefficient indexes + +-- Drop low-selectivity single-column indexes (status only has 2 values) +DROP INDEX IF EXISTS idx_supervision_status; +DROP INDEX IF EXISTS idx_notification_read_status; + +-- Drop redundant tenant_id indexes (replaced by composite indexes) +DROP INDEX IF EXISTS idx_supervision_tenant_id; +DROP INDEX IF EXISTS idx_notification_tenant_id; +DROP INDEX IF EXISTS idx_task_transfer_tenant_id; +DROP INDEX IF EXISTS idx_assignee_op_tenant_id; +DROP INDEX IF EXISTS idx_rollback_tenant_id; + +-- Drop single-column indexes replaced by composite indexes +DROP INDEX IF EXISTS idx_supervision_task_instance_id; +DROP INDEX IF EXISTS idx_supervision_supervisor_user_id; +DROP INDEX IF EXISTS idx_notification_receiver_user_id; +DROP INDEX IF EXISTS idx_notification_sender_user_id; +DROP INDEX IF EXISTS idx_task_transfer_task_instance_id; +DROP INDEX IF EXISTS idx_assignee_op_task_instance_id; +DROP INDEX IF EXISTS idx_rollback_process_instance_id; + +-- Step 3: Update statistics +ANALYZE se_supervision_instance; +ANALYZE se_notification_instance; +ANALYZE se_task_transfer_record; +ANALYZE se_assignee_operation_record; +ANALYZE se_process_rollback_record; + +-- =========================================== +-- Verification query +-- =========================================== +-- SELECT schemaname, tablename, indexname, indexdef +-- FROM pg_indexes +-- WHERE tablename IN ( +-- 'se_supervision_instance', +-- 'se_notification_instance', +-- 'se_task_transfer_record', +-- 'se_assignee_operation_record', +-- 'se_process_rollback_record' +-- ) +-- ORDER BY tablename, indexname; diff --git a/extension/storage/storage-mysql/src/main/resources/sql/migration/V20260209_2__create_index_tables.sql b/extension/storage/storage-mysql/src/main/resources/sql/migration/V20260209_2__create_index_tables.sql new file mode 100644 index 000000000..cc8522bac --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/migration/V20260209_2__create_index_tables.sql @@ -0,0 +1,46 @@ +-- Create user task index table and user notification index table (MySQL version) +-- These tables support fast user-centric queries in sharding mode + +-- =========================================== +-- User task index table (not partitioned) +-- Stores only active/pending tasks for fast user-centric queries +-- =========================================== +CREATE TABLE IF NOT EXISTS `se_user_task_index` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `tenant_id` varchar(64) DEFAULT NULL, + `assignee_id` varchar(255) NOT NULL, + `assignee_type` varchar(128) NOT NULL DEFAULT 'user', + `task_instance_id` bigint(20) unsigned NOT NULL, + `process_instance_id` bigint(20) unsigned NOT NULL, + `process_definition_type` varchar(255) DEFAULT NULL, + `domain_code` varchar(64) DEFAULT NULL, + `extra` json DEFAULT NULL, + `task_status` varchar(64) NOT NULL, + `task_gmt_modified` datetime(6) DEFAULT NULL, + `title` varchar(255) DEFAULT NULL, + `priority` int(11) DEFAULT 500, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_task_idx` (`tenant_id`, `assignee_id`, `task_instance_id`), + KEY `idx_user_task_pending` (`tenant_id`, `assignee_id`, `assignee_type`, `task_status`), + KEY `idx_user_task_type` (`tenant_id`, `assignee_id`, `process_definition_type`, `task_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='User task index for sharding mode'; + +-- =========================================== +-- User notification index table (not partitioned) +-- Stores only unread notifications for fast user-centric queries +-- =========================================== +CREATE TABLE IF NOT EXISTS `se_user_notification_index` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `tenant_id` varchar(64) DEFAULT NULL, + `receiver_user_id` varchar(255) NOT NULL, + `notification_id` bigint(20) unsigned NOT NULL, + `process_instance_id` bigint(20) unsigned NOT NULL, + `notification_type` varchar(64) NOT NULL, + `title` varchar(255) DEFAULT NULL, + `read_status` varchar(64) NOT NULL DEFAULT 'unread', + `gmt_create` datetime(6) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_notif_idx` (`tenant_id`, `receiver_user_id`, `notification_id`), + KEY `idx_user_notif_receiver` (`tenant_id`, `receiver_user_id`, `read_status`), + KEY `idx_user_notif_type` (`tenant_id`, `receiver_user_id`, `notification_type`, `read_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='User notification index for sharding mode'; diff --git a/extension/storage/storage-mysql/src/main/resources/sql/migration/V20260209_2__create_index_tables_postgresql.sql b/extension/storage/storage-mysql/src/main/resources/sql/migration/V20260209_2__create_index_tables_postgresql.sql new file mode 100644 index 000000000..6b353fa2b --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/migration/V20260209_2__create_index_tables_postgresql.sql @@ -0,0 +1,78 @@ +-- Create user task index table and user notification index table (PostgreSQL version) +-- These tables support fast user-centric queries in sharding mode + +-- =========================================== +-- User task index table (not partitioned) +-- Stores only active/pending tasks for fast user-centric queries +-- =========================================== +CREATE TABLE IF NOT EXISTS se_user_task_index ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + tenant_id varchar(64), + assignee_id varchar(255) NOT NULL, + assignee_type varchar(128) NOT NULL DEFAULT 'user', + task_instance_id bigint NOT NULL, + process_instance_id bigint NOT NULL, + process_definition_type varchar(255), + domain_code varchar(64), + extra jsonb, + task_status varchar(64) NOT NULL, + task_gmt_modified timestamp(6), + title varchar(255), + priority int DEFAULT 500, + PRIMARY KEY (id) +); + +-- Unique constraint for deduplication +CREATE UNIQUE INDEX IF NOT EXISTS uk_user_task_idx + ON se_user_task_index (tenant_id, assignee_id, task_instance_id); + +-- Full index for general status queries +CREATE INDEX IF NOT EXISTS idx_user_task_status + ON se_user_task_index (tenant_id, assignee_id, assignee_type, task_status); + +-- Partial index for pending tasks only (most frequent query) +CREATE INDEX IF NOT EXISTS idx_user_task_pending + ON se_user_task_index (tenant_id, assignee_id, assignee_type, task_status) + WHERE task_status = 'pending'; + +-- Index for process definition type queries +CREATE INDEX IF NOT EXISTS idx_user_task_type + ON se_user_task_index (tenant_id, assignee_id, process_definition_type, task_status); + +COMMENT ON TABLE se_user_task_index IS 'User task index for sharding mode'; + +-- =========================================== +-- User notification index table (not partitioned) +-- Stores only unread notifications for fast user-centric queries +-- =========================================== +CREATE TABLE IF NOT EXISTS se_user_notification_index ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + tenant_id varchar(64), + receiver_user_id varchar(255) NOT NULL, + notification_id bigint NOT NULL, + process_instance_id bigint NOT NULL, + notification_type varchar(64) NOT NULL, + title varchar(255), + read_status varchar(64) NOT NULL DEFAULT 'unread', + gmt_create timestamp(6) NOT NULL, + PRIMARY KEY (id) +); + +-- Unique constraint for deduplication +CREATE UNIQUE INDEX IF NOT EXISTS uk_user_notif_idx + ON se_user_notification_index (tenant_id, receiver_user_id, notification_id); + +-- Full index for receiver queries +CREATE INDEX IF NOT EXISTS idx_user_notif_receiver + ON se_user_notification_index (tenant_id, receiver_user_id, read_status); + +-- Partial index for unread notifications only (most frequent query) +CREATE INDEX IF NOT EXISTS idx_user_notif_unread + ON se_user_notification_index (tenant_id, receiver_user_id, read_status) + WHERE read_status = 'unread'; + +-- Index for notification type queries +CREATE INDEX IF NOT EXISTS idx_user_notif_type + ON se_user_notification_index (tenant_id, receiver_user_id, notification_type, read_status); + +COMMENT ON TABLE se_user_notification_index IS 'User notification index for sharding mode'; diff --git a/extension/storage/storage-mysql/src/main/resources/sql/migration/V20260209_3__backfill_data.sql b/extension/storage/storage-mysql/src/main/resources/sql/migration/V20260209_3__backfill_data.sql new file mode 100644 index 000000000..0ae0dd77d --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/migration/V20260209_3__backfill_data.sql @@ -0,0 +1,71 @@ +-- Data backfill migration script (MySQL version) +-- Phase 3: Populate process_instance_id and build index tables from existing data + +-- =========================================== +-- Step 1: Backfill process_instance_id in se_task_transfer_record +-- =========================================== +UPDATE se_task_transfer_record r +INNER JOIN se_task_instance t ON r.task_instance_id = t.id +SET r.process_instance_id = t.process_instance_id +WHERE r.process_instance_id IS NULL; + +-- =========================================== +-- Step 2: Backfill process_instance_id in se_assignee_operation_record +-- =========================================== +UPDATE se_assignee_operation_record r +INNER JOIN se_task_instance t ON r.task_instance_id = t.id +SET r.process_instance_id = t.process_instance_id +WHERE r.process_instance_id IS NULL; + +-- =========================================== +-- Step 3: Full build se_user_task_index from existing data +-- Only insert active/pending tasks (not completed/canceled/aborted) +-- =========================================== +INSERT INTO se_user_task_index + (tenant_id, assignee_id, assignee_type, task_instance_id, process_instance_id, + process_definition_type, domain_code, extra, task_status, task_gmt_modified, + title, priority) +SELECT + ta.tenant_id, + ta.assignee_id, + ta.assignee_type, + ta.task_instance_id, + ta.process_instance_id, + t.process_definition_type, + t.domain_code, + t.extra, + t.status, + t.gmt_modified, + t.title, + t.priority +FROM se_task_assignee_instance ta +INNER JOIN se_task_instance t ON ta.task_instance_id = t.id + AND ta.process_instance_id = t.process_instance_id +WHERE t.status NOT IN ('completed', 'canceled', 'aborted') +ON DUPLICATE KEY UPDATE + task_status = VALUES(task_status), + task_gmt_modified = VALUES(task_gmt_modified), + title = VALUES(title), + priority = VALUES(priority); + +-- =========================================== +-- Step 4: Full build se_user_notification_index from existing data +-- Only insert unread notifications +-- =========================================== +INSERT INTO se_user_notification_index + (tenant_id, receiver_user_id, notification_id, process_instance_id, + notification_type, title, read_status, gmt_create) +SELECT + n.tenant_id, + n.receiver_user_id, + n.id, + n.process_instance_id, + n.notification_type, + n.title, + n.read_status, + n.gmt_create +FROM se_notification_instance n +WHERE n.read_status = 'unread' +ON DUPLICATE KEY UPDATE + read_status = VALUES(read_status), + title = VALUES(title); diff --git a/extension/storage/storage-mysql/src/main/resources/sql/migration/V20260209_3__backfill_data_postgresql.sql b/extension/storage/storage-mysql/src/main/resources/sql/migration/V20260209_3__backfill_data_postgresql.sql new file mode 100644 index 000000000..822cad7be --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/migration/V20260209_3__backfill_data_postgresql.sql @@ -0,0 +1,73 @@ +-- Data backfill migration script (PostgreSQL version) +-- Phase 3: Populate process_instance_id and build index tables from existing data + +-- =========================================== +-- Step 1: Backfill process_instance_id in se_task_transfer_record +-- =========================================== +UPDATE se_task_transfer_record r +SET process_instance_id = t.process_instance_id +FROM se_task_instance t +WHERE r.task_instance_id = t.id + AND r.process_instance_id IS NULL; + +-- =========================================== +-- Step 2: Backfill process_instance_id in se_assignee_operation_record +-- =========================================== +UPDATE se_assignee_operation_record r +SET process_instance_id = t.process_instance_id +FROM se_task_instance t +WHERE r.task_instance_id = t.id + AND r.process_instance_id IS NULL; + +-- =========================================== +-- Step 3: Full build se_user_task_index from existing data +-- Only insert active/pending tasks (not completed/canceled/aborted) +-- =========================================== +INSERT INTO se_user_task_index + (tenant_id, assignee_id, assignee_type, task_instance_id, process_instance_id, + process_definition_type, domain_code, extra, task_status, task_gmt_modified, + title, priority) +SELECT + ta.tenant_id, + ta.assignee_id, + ta.assignee_type, + ta.task_instance_id, + ta.process_instance_id, + t.process_definition_type, + t.domain_code, + t.extra, + t.status, + t.gmt_modified, + t.title, + t.priority +FROM se_task_assignee_instance ta +INNER JOIN se_task_instance t ON ta.task_instance_id = t.id + AND ta.process_instance_id = t.process_instance_id +WHERE t.status NOT IN ('completed', 'canceled', 'aborted') +ON CONFLICT (tenant_id, assignee_id, task_instance_id) DO UPDATE SET + task_status = EXCLUDED.task_status, + task_gmt_modified = EXCLUDED.task_gmt_modified, + title = EXCLUDED.title, + priority = EXCLUDED.priority; + +-- =========================================== +-- Step 4: Full build se_user_notification_index from existing data +-- Only insert unread notifications +-- =========================================== +INSERT INTO se_user_notification_index + (tenant_id, receiver_user_id, notification_id, process_instance_id, + notification_type, title, read_status, gmt_create) +SELECT + n.tenant_id, + n.receiver_user_id, + n.id, + n.process_instance_id, + n.notification_type, + n.title, + n.read_status, + n.gmt_create +FROM se_notification_instance n +WHERE n.read_status = 'unread' +ON CONFLICT (tenant_id, receiver_user_id, notification_id) DO UPDATE SET + read_status = EXCLUDED.read_status, + title = EXCLUDED.title; diff --git a/extension/storage/storage-mysql/src/main/resources/sql/migration/V20260209__add_process_instance_id_to_transfer_and_operation.sql b/extension/storage/storage-mysql/src/main/resources/sql/migration/V20260209__add_process_instance_id_to_transfer_and_operation.sql new file mode 100644 index 000000000..ea14e8f5e --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/migration/V20260209__add_process_instance_id_to_transfer_and_operation.sql @@ -0,0 +1,7 @@ +-- Add process_instance_id column to se_task_transfer_record +ALTER TABLE se_task_transfer_record ADD COLUMN process_instance_id bigint(20) unsigned DEFAULT NULL COMMENT 'process instance id' AFTER task_instance_id; +CREATE INDEX idx_transfer_process_instance_id ON se_task_transfer_record (process_instance_id); + +-- Add process_instance_id column to se_assignee_operation_record +ALTER TABLE se_assignee_operation_record ADD COLUMN process_instance_id bigint(20) unsigned DEFAULT NULL COMMENT 'process instance id' AFTER task_instance_id; +CREATE INDEX idx_assignee_op_process_instance_id ON se_assignee_operation_record (process_instance_id); diff --git a/extension/storage/storage-mysql/src/main/resources/sql/schema-h2-only-for-test.sql b/extension/storage/storage-mysql/src/main/resources/sql/schema-h2-only-for-test.sql index 9395b3983..181a9dd6a 100644 --- a/extension/storage/storage-mysql/src/main/resources/sql/schema-h2-only-for-test.sql +++ b/extension/storage/storage-mysql/src/main/resources/sql/schema-h2-only-for-test.sql @@ -67,6 +67,8 @@ CREATE TABLE IF NOT EXISTS `se_task_instance` ( `status` varchar(255) NOT NULL COMMENT 'status' , `comment` varchar(255) DEFAULT NULL COMMENT 'comment' , `extension` varchar(255) DEFAULT NULL COMMENT 'extension' , + `domain_code` varchar(64) DEFAULT NULL COMMENT 'domain code' , + `extra` clob DEFAULT NULL COMMENT 'extra JSON data' , `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id' , PRIMARY KEY (`id`) diff --git a/extension/storage/storage-mysql/src/main/resources/sql/schema-oracle.sql b/extension/storage/storage-mysql/src/main/resources/sql/schema-oracle.sql new file mode 100644 index 000000000..71ed06f6d --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/schema-oracle.sql @@ -0,0 +1,249 @@ +-- Oracle/DM (DaMeng) database schema for SmartEngine +-- Compatible with both Oracle Database and DaMeng Database +-- Based on SmartEngine existing table structure design + +-- =========================================== +-- Sequences +-- =========================================== +CREATE SEQUENCE se_deployment_instance_seq START WITH 1 INCREMENT BY 1; +CREATE SEQUENCE se_process_instance_seq START WITH 1 INCREMENT BY 1; +CREATE SEQUENCE se_activity_instance_seq START WITH 1 INCREMENT BY 1; +CREATE SEQUENCE se_task_instance_seq START WITH 1 INCREMENT BY 1; +CREATE SEQUENCE se_execution_instance_seq START WITH 1 INCREMENT BY 1; +CREATE SEQUENCE se_task_assignee_instance_seq START WITH 1 INCREMENT BY 1; +CREATE SEQUENCE se_variable_instance_seq START WITH 1 INCREMENT BY 1; + +-- =========================================== +-- Deployment instance table +-- =========================================== +CREATE TABLE se_deployment_instance ( + id NUMBER(19) DEFAULT se_deployment_instance_seq.NEXTVAL NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_definition_id VARCHAR2(255) NOT NULL, + process_definition_version VARCHAR2(255) DEFAULT NULL, + process_definition_type VARCHAR2(255) DEFAULT NULL, + process_definition_code VARCHAR2(255) DEFAULT NULL, + process_definition_name VARCHAR2(255) DEFAULT NULL, + process_definition_desc VARCHAR2(255) DEFAULT NULL, + process_definition_content CLOB NOT NULL, + deployment_user_id VARCHAR2(128) NOT NULL, + deployment_status VARCHAR2(64) NOT NULL, + logic_status VARCHAR2(64) NOT NULL, + extension CLOB DEFAULT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + CONSTRAINT pk_deployment_instance PRIMARY KEY (id) +); + +COMMENT ON TABLE se_deployment_instance IS 'Deployment instance table'; +COMMENT ON COLUMN se_deployment_instance.id IS 'PK'; +COMMENT ON COLUMN se_deployment_instance.gmt_create IS 'create time'; +COMMENT ON COLUMN se_deployment_instance.gmt_modified IS 'modification time'; +COMMENT ON COLUMN se_deployment_instance.process_definition_id IS 'process definition id'; +COMMENT ON COLUMN se_deployment_instance.process_definition_version IS 'process definition version'; +COMMENT ON COLUMN se_deployment_instance.process_definition_type IS 'process definition type'; +COMMENT ON COLUMN se_deployment_instance.process_definition_code IS 'process definition code'; +COMMENT ON COLUMN se_deployment_instance.process_definition_name IS 'process definition name'; +COMMENT ON COLUMN se_deployment_instance.process_definition_desc IS 'process definition desc'; +COMMENT ON COLUMN se_deployment_instance.process_definition_content IS 'process definition content'; +COMMENT ON COLUMN se_deployment_instance.deployment_user_id IS 'deployment user id'; +COMMENT ON COLUMN se_deployment_instance.deployment_status IS 'deployment status'; +COMMENT ON COLUMN se_deployment_instance.logic_status IS 'logic status'; +COMMENT ON COLUMN se_deployment_instance.extension IS 'extension'; +COMMENT ON COLUMN se_deployment_instance.tenant_id IS 'tenant id'; + +-- =========================================== +-- Process instance table +-- =========================================== +CREATE TABLE se_process_instance ( + id NUMBER(19) DEFAULT se_process_instance_seq.NEXTVAL NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_definition_id_and_version VARCHAR2(128) NOT NULL, + process_definition_type VARCHAR2(255) DEFAULT NULL, + status VARCHAR2(64) NOT NULL, + parent_process_instance_id NUMBER(19) DEFAULT NULL, + parent_execution_instance_id NUMBER(19) DEFAULT NULL, + start_user_id VARCHAR2(128) DEFAULT NULL, + biz_unique_id VARCHAR2(255) DEFAULT NULL, + reason VARCHAR2(255) DEFAULT NULL, + comment VARCHAR2(255) DEFAULT NULL, + title VARCHAR2(255) DEFAULT NULL, + tag VARCHAR2(255) DEFAULT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + CONSTRAINT pk_process_instance PRIMARY KEY (id) +); + +COMMENT ON TABLE se_process_instance IS 'Process instance table'; +COMMENT ON COLUMN se_process_instance.id IS 'PK'; +COMMENT ON COLUMN se_process_instance.gmt_create IS 'create time'; +COMMENT ON COLUMN se_process_instance.gmt_modified IS 'modification time'; +COMMENT ON COLUMN se_process_instance.process_definition_id_and_version IS 'process definition id and version'; +COMMENT ON COLUMN se_process_instance.process_definition_type IS 'process definition type'; +COMMENT ON COLUMN se_process_instance.status IS '1.running 2.completed 3.aborted'; +COMMENT ON COLUMN se_process_instance.parent_process_instance_id IS 'parent process instance id'; +COMMENT ON COLUMN se_process_instance.parent_execution_instance_id IS 'parent execution instance id'; +COMMENT ON COLUMN se_process_instance.start_user_id IS 'start user id'; +COMMENT ON COLUMN se_process_instance.biz_unique_id IS 'biz unique id'; +COMMENT ON COLUMN se_process_instance.reason IS 'reason'; +COMMENT ON COLUMN se_process_instance.comment IS 'comment'; +COMMENT ON COLUMN se_process_instance.title IS 'title'; +COMMENT ON COLUMN se_process_instance.tag IS 'tag'; +COMMENT ON COLUMN se_process_instance.tenant_id IS 'tenant id'; + +-- =========================================== +-- Activity instance table +-- =========================================== +CREATE TABLE se_activity_instance ( + id NUMBER(19) DEFAULT se_activity_instance_seq.NEXTVAL NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_instance_id NUMBER(19) DEFAULT NULL, + process_definition_id_and_version VARCHAR2(255) NOT NULL, + process_definition_activity_id VARCHAR2(64) NOT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + CONSTRAINT pk_activity_instance PRIMARY KEY (id) +); + +COMMENT ON TABLE se_activity_instance IS 'Activity instance table'; +COMMENT ON COLUMN se_activity_instance.id IS 'PK'; +COMMENT ON COLUMN se_activity_instance.gmt_create IS 'create time'; +COMMENT ON COLUMN se_activity_instance.gmt_modified IS 'modification time'; +COMMENT ON COLUMN se_activity_instance.process_instance_id IS 'process instance id'; +COMMENT ON COLUMN se_activity_instance.process_definition_id_and_version IS 'process definition id and version'; +COMMENT ON COLUMN se_activity_instance.process_definition_activity_id IS 'process definition activity id'; +COMMENT ON COLUMN se_activity_instance.tenant_id IS 'tenant id'; + +-- =========================================== +-- Task instance table +-- =========================================== +CREATE TABLE se_task_instance ( + id NUMBER(19) DEFAULT se_task_instance_seq.NEXTVAL NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_instance_id NUMBER(19) NOT NULL, + process_definition_id_and_version VARCHAR2(128) DEFAULT NULL, + process_definition_type VARCHAR2(255) DEFAULT NULL, + activity_instance_id NUMBER(19) NOT NULL, + process_definition_activity_id VARCHAR2(255) NOT NULL, + execution_instance_id NUMBER(19) NOT NULL, + claim_user_id VARCHAR2(255) DEFAULT NULL, + title VARCHAR2(255) DEFAULT NULL, + priority NUMBER(10) DEFAULT 500, + tag VARCHAR2(255) DEFAULT NULL, + claim_time TIMESTAMP(6) DEFAULT NULL, + complete_time TIMESTAMP(6) DEFAULT NULL, + status VARCHAR2(255) NOT NULL, + comment VARCHAR2(255) DEFAULT NULL, + extension VARCHAR2(255) DEFAULT NULL, + domain_code VARCHAR2(64) DEFAULT NULL, + extra CLOB DEFAULT NULL CHECK (extra IS JSON), + tenant_id VARCHAR2(64) DEFAULT NULL, + CONSTRAINT pk_task_instance PRIMARY KEY (id) +); + +COMMENT ON TABLE se_task_instance IS 'Task instance table'; +COMMENT ON COLUMN se_task_instance.id IS 'PK'; +COMMENT ON COLUMN se_task_instance.gmt_create IS 'create time'; +COMMENT ON COLUMN se_task_instance.gmt_modified IS 'modification time'; +COMMENT ON COLUMN se_task_instance.process_instance_id IS 'process instance id'; +COMMENT ON COLUMN se_task_instance.process_definition_id_and_version IS 'process definition id and version'; +COMMENT ON COLUMN se_task_instance.process_definition_type IS 'process definition type'; +COMMENT ON COLUMN se_task_instance.activity_instance_id IS 'activity instance id'; +COMMENT ON COLUMN se_task_instance.process_definition_activity_id IS 'process definition activity id'; +COMMENT ON COLUMN se_task_instance.execution_instance_id IS 'execution instance id'; +COMMENT ON COLUMN se_task_instance.claim_user_id IS 'claim user id'; +COMMENT ON COLUMN se_task_instance.title IS 'title'; +COMMENT ON COLUMN se_task_instance.priority IS 'priority'; +COMMENT ON COLUMN se_task_instance.tag IS 'tag'; +COMMENT ON COLUMN se_task_instance.claim_time IS 'claim time'; +COMMENT ON COLUMN se_task_instance.complete_time IS 'complete time'; +COMMENT ON COLUMN se_task_instance.status IS 'status'; +COMMENT ON COLUMN se_task_instance.comment IS 'comment'; +COMMENT ON COLUMN se_task_instance.extension IS 'extension'; +COMMENT ON COLUMN se_task_instance.tenant_id IS 'tenant id'; + +-- =========================================== +-- Execution instance table +-- =========================================== +CREATE TABLE se_execution_instance ( + id NUMBER(19) DEFAULT se_execution_instance_seq.NEXTVAL NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_instance_id NUMBER(19) NOT NULL, + process_definition_id_and_version VARCHAR2(255) NOT NULL, + process_definition_activity_id VARCHAR2(255) NOT NULL, + activity_instance_id NUMBER(19) NOT NULL, + block_id NUMBER(19) DEFAULT NULL, + active NUMBER(1) NOT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + CONSTRAINT pk_execution_instance PRIMARY KEY (id) +); + +COMMENT ON TABLE se_execution_instance IS 'Execution instance table'; +COMMENT ON COLUMN se_execution_instance.id IS 'PK'; +COMMENT ON COLUMN se_execution_instance.gmt_create IS 'create time'; +COMMENT ON COLUMN se_execution_instance.gmt_modified IS 'modification time'; +COMMENT ON COLUMN se_execution_instance.process_instance_id IS 'process instance id'; +COMMENT ON COLUMN se_execution_instance.process_definition_id_and_version IS 'process definition id and version'; +COMMENT ON COLUMN se_execution_instance.process_definition_activity_id IS 'process definition activity id'; +COMMENT ON COLUMN se_execution_instance.activity_instance_id IS 'activity instance id'; +COMMENT ON COLUMN se_execution_instance.block_id IS 'block id'; +COMMENT ON COLUMN se_execution_instance.active IS '1:active 0:inactive'; +COMMENT ON COLUMN se_execution_instance.tenant_id IS 'tenant id'; + +-- =========================================== +-- Task assignee instance table +-- =========================================== +CREATE TABLE se_task_assignee_instance ( + id NUMBER(19) DEFAULT se_task_assignee_instance_seq.NEXTVAL NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_instance_id NUMBER(19) NOT NULL, + task_instance_id NUMBER(19) NOT NULL, + assignee_id VARCHAR2(255) NOT NULL, + assignee_type VARCHAR2(128) NOT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + CONSTRAINT pk_task_assignee_instance PRIMARY KEY (id) +); + +COMMENT ON TABLE se_task_assignee_instance IS 'Task assignee instance table'; +COMMENT ON COLUMN se_task_assignee_instance.id IS 'PK'; +COMMENT ON COLUMN se_task_assignee_instance.gmt_create IS 'create time'; +COMMENT ON COLUMN se_task_assignee_instance.gmt_modified IS 'modification time'; +COMMENT ON COLUMN se_task_assignee_instance.process_instance_id IS 'process instance id'; +COMMENT ON COLUMN se_task_assignee_instance.task_instance_id IS 'task instance id'; +COMMENT ON COLUMN se_task_assignee_instance.assignee_id IS 'assignee id'; +COMMENT ON COLUMN se_task_assignee_instance.assignee_type IS 'assignee type'; +COMMENT ON COLUMN se_task_assignee_instance.tenant_id IS 'tenant id'; + +-- =========================================== +-- Variable instance table +-- =========================================== +CREATE TABLE se_variable_instance ( + id NUMBER(19) DEFAULT se_variable_instance_seq.NEXTVAL NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_instance_id NUMBER(19) NOT NULL, + execution_instance_id NUMBER(19) DEFAULT NULL, + field_key VARCHAR2(128) NOT NULL, + field_type VARCHAR2(128) NOT NULL, + field_double_value NUMBER(65,30) DEFAULT NULL, + field_long_value NUMBER(19) DEFAULT NULL, + field_string_value VARCHAR2(4000) DEFAULT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + CONSTRAINT pk_variable_instance PRIMARY KEY (id) +); + +COMMENT ON TABLE se_variable_instance IS 'Variable instance table'; +COMMENT ON COLUMN se_variable_instance.id IS 'PK'; +COMMENT ON COLUMN se_variable_instance.gmt_create IS 'create time'; +COMMENT ON COLUMN se_variable_instance.gmt_modified IS 'modification time'; +COMMENT ON COLUMN se_variable_instance.process_instance_id IS 'process instance id'; +COMMENT ON COLUMN se_variable_instance.execution_instance_id IS 'execution instance id'; +COMMENT ON COLUMN se_variable_instance.field_key IS 'field key'; +COMMENT ON COLUMN se_variable_instance.field_type IS 'field type'; +COMMENT ON COLUMN se_variable_instance.field_double_value IS 'field double value'; +COMMENT ON COLUMN se_variable_instance.field_long_value IS 'field long value'; +COMMENT ON COLUMN se_variable_instance.field_string_value IS 'field string value'; +COMMENT ON COLUMN se_variable_instance.tenant_id IS 'tenant id'; diff --git a/extension/storage/storage-mysql/src/main/resources/sql/schema-postgre.sql b/extension/storage/storage-mysql/src/main/resources/sql/schema-postgre.sql new file mode 100644 index 000000000..20ca4f44b --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/schema-postgre.sql @@ -0,0 +1,114 @@ +CREATE TABLE se_deployment_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_definition_id varchar(255) NOT NULL, + process_definition_version varchar(255), + process_definition_type varchar(255), + process_definition_code varchar(255), + process_definition_name varchar(255), + process_definition_desc varchar(255), + process_definition_content text NOT NULL, + deployment_user_id varchar(128) NOT NULL, + deployment_status varchar(64) NOT NULL, + logic_status varchar(64) NOT NULL, + extension text, + tenant_id varchar(64), + PRIMARY KEY (id) +); + +CREATE TABLE se_process_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_definition_id_and_version varchar(128) NOT NULL, + process_definition_type varchar(255), + status varchar(64) NOT NULL, + parent_process_instance_id bigint, + parent_execution_instance_id bigint, + start_user_id varchar(128), + biz_unique_id varchar(255), + reason varchar(255), + comment varchar(255), + title varchar(255), + tag varchar(255), + tenant_id varchar(64), + PRIMARY KEY (id) +); + +CREATE TABLE se_activity_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint, + process_definition_id_and_version varchar(255) NOT NULL, + process_definition_activity_id varchar(64) NOT NULL, + tenant_id varchar(64), + PRIMARY KEY (id) +); + +CREATE TABLE se_task_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + process_definition_id_and_version varchar(128), + process_definition_type varchar(255), + activity_instance_id bigint NOT NULL, + process_definition_activity_id varchar(255) NOT NULL, + execution_instance_id bigint NOT NULL, + claim_user_id varchar(255), + title varchar(255), + priority int DEFAULT 500, + tag varchar(255), + claim_time timestamp(6), + complete_time timestamp(6), + status varchar(255) NOT NULL, + comment varchar(255), + extension varchar(255), + domain_code varchar(64) DEFAULT NULL, + extra jsonb DEFAULT NULL, + tenant_id varchar(64), + PRIMARY KEY (id) +); + +CREATE TABLE se_execution_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + process_definition_id_and_version varchar(255) NOT NULL, + process_definition_activity_id varchar(255) NOT NULL, + activity_instance_id bigint NOT NULL, + block_id bigint, + active smallint NOT NULL, + tenant_id varchar(64), + PRIMARY KEY (id) +); + +CREATE TABLE se_task_assignee_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint NOT NULL, + assignee_id varchar(255) NOT NULL, + assignee_type varchar(128) NOT NULL, + tenant_id varchar(64), + PRIMARY KEY (id) +); + +CREATE TABLE se_variable_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + execution_instance_id bigint, + field_key varchar(128) NOT NULL, + field_type varchar(128) NOT NULL, + field_double_value decimal(65,30), + field_long_value bigint, + field_string_value varchar(4000), + tenant_id varchar(64), + PRIMARY KEY (id) +); \ No newline at end of file diff --git a/extension/storage/storage-mysql/src/main/resources/sql/schema.sql b/extension/storage/storage-mysql/src/main/resources/sql/schema.sql index 4b350d951..f7ead50de 100644 --- a/extension/storage/storage-mysql/src/main/resources/sql/schema.sql +++ b/extension/storage/storage-mysql/src/main/resources/sql/schema.sql @@ -70,6 +70,8 @@ CREATE TABLE `se_task_instance` ( `status` varchar(255) NOT NULL COMMENT 'status' , `comment` varchar(255) DEFAULT NULL COMMENT 'comment' , `extension` varchar(255) DEFAULT NULL COMMENT 'extension' , + `domain_code` varchar(64) DEFAULT NULL COMMENT 'domain code' , + `extra` json DEFAULT NULL COMMENT 'extra JSON data' , `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id' , PRIMARY KEY (`id`) diff --git a/extension/storage/storage-mysql/src/main/resources/sql/sharding/index-tables-mysql.sql b/extension/storage/storage-mysql/src/main/resources/sql/sharding/index-tables-mysql.sql new file mode 100644 index 000000000..b3890ce67 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/sharding/index-tables-mysql.sql @@ -0,0 +1,49 @@ +-- =========================================== +-- Index tables for sharding mode - MySQL version +-- These tables are NOT partitioned +-- They serve as global indexes for user-centric queries +-- =========================================== + +-- =========================================== +-- User task index table (not partitioned) +-- Stores only active/pending tasks for fast user-centric queries +-- =========================================== +CREATE TABLE `se_user_task_index` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `tenant_id` varchar(64) DEFAULT NULL, + `assignee_id` varchar(255) NOT NULL, + `assignee_type` varchar(128) NOT NULL DEFAULT 'user', + `task_instance_id` bigint(20) unsigned NOT NULL, + `process_instance_id` bigint(20) unsigned NOT NULL, + `process_definition_type` varchar(255) DEFAULT NULL, + `domain_code` varchar(64) DEFAULT NULL, + `extra` json DEFAULT NULL, + `task_status` varchar(64) NOT NULL, + `task_gmt_modified` datetime(6) DEFAULT NULL, + `title` varchar(255) DEFAULT NULL, + `priority` int(11) DEFAULT 500, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_task_idx` (`tenant_id`, `assignee_id`, `task_instance_id`), + KEY `idx_user_task_pending` (`tenant_id`, `assignee_id`, `assignee_type`, `task_status`), + KEY `idx_user_task_type` (`tenant_id`, `assignee_id`, `process_definition_type`, `task_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='User task index for sharding mode'; + +-- =========================================== +-- User notification index table (not partitioned) +-- Stores only unread notifications for fast user-centric queries +-- =========================================== +CREATE TABLE `se_user_notification_index` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `tenant_id` varchar(64) DEFAULT NULL, + `receiver_user_id` varchar(255) NOT NULL, + `notification_id` bigint(20) unsigned NOT NULL, + `process_instance_id` bigint(20) unsigned NOT NULL, + `notification_type` varchar(64) NOT NULL, + `title` varchar(255) DEFAULT NULL, + `read_status` varchar(64) NOT NULL DEFAULT 'unread', + `gmt_create` datetime(6) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_notif_idx` (`tenant_id`, `receiver_user_id`, `notification_id`), + KEY `idx_user_notif_receiver` (`tenant_id`, `receiver_user_id`, `read_status`), + KEY `idx_user_notif_type` (`tenant_id`, `receiver_user_id`, `notification_type`, `read_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='User notification index for sharding mode'; diff --git a/extension/storage/storage-mysql/src/main/resources/sql/sharding/index-tables-postgresql.sql b/extension/storage/storage-mysql/src/main/resources/sql/sharding/index-tables-postgresql.sql new file mode 100644 index 000000000..86586c347 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/sharding/index-tables-postgresql.sql @@ -0,0 +1,81 @@ +-- =========================================== +-- Index tables for sharding mode - PostgreSQL version +-- These tables are NOT partitioned +-- They serve as global indexes for user-centric queries +-- =========================================== + +-- =========================================== +-- User task index table (not partitioned) +-- Stores only active/pending tasks for fast user-centric queries +-- =========================================== +CREATE TABLE se_user_task_index ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + tenant_id varchar(64), + assignee_id varchar(255) NOT NULL, + assignee_type varchar(128) NOT NULL DEFAULT 'user', + task_instance_id bigint NOT NULL, + process_instance_id bigint NOT NULL, + process_definition_type varchar(255), + domain_code varchar(64), + extra jsonb, + task_status varchar(64) NOT NULL, + task_gmt_modified timestamp(6), + title varchar(255), + priority int DEFAULT 500, + PRIMARY KEY (id) +); + +-- Unique constraint for deduplication +CREATE UNIQUE INDEX uk_user_task_idx + ON se_user_task_index (tenant_id, assignee_id, task_instance_id); + +-- Full index for general status queries +CREATE INDEX idx_user_task_status + ON se_user_task_index (tenant_id, assignee_id, assignee_type, task_status); + +-- Partial index for pending tasks only (most frequent query) +CREATE INDEX idx_user_task_pending + ON se_user_task_index (tenant_id, assignee_id, assignee_type, task_status) + WHERE task_status = 'pending'; + +-- Index for process definition type queries +CREATE INDEX idx_user_task_type + ON se_user_task_index (tenant_id, assignee_id, process_definition_type, task_status); + +COMMENT ON TABLE se_user_task_index IS 'User task index for sharding mode'; + +-- =========================================== +-- User notification index table (not partitioned) +-- Stores only unread notifications for fast user-centric queries +-- =========================================== +CREATE TABLE se_user_notification_index ( + id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, + tenant_id varchar(64), + receiver_user_id varchar(255) NOT NULL, + notification_id bigint NOT NULL, + process_instance_id bigint NOT NULL, + notification_type varchar(64) NOT NULL, + title varchar(255), + read_status varchar(64) NOT NULL DEFAULT 'unread', + gmt_create timestamp(6) NOT NULL, + PRIMARY KEY (id) +); + +-- Unique constraint for deduplication +CREATE UNIQUE INDEX uk_user_notif_idx + ON se_user_notification_index (tenant_id, receiver_user_id, notification_id); + +-- Full index for receiver queries +CREATE INDEX idx_user_notif_receiver + ON se_user_notification_index (tenant_id, receiver_user_id, read_status); + +-- Partial index for unread notifications only (most frequent query) +CREATE INDEX idx_user_notif_unread + ON se_user_notification_index (tenant_id, receiver_user_id, read_status) + WHERE read_status = 'unread'; + +-- Index for notification type queries +CREATE INDEX idx_user_notif_type + ON se_user_notification_index (tenant_id, receiver_user_id, notification_type, read_status); + +COMMENT ON TABLE se_user_notification_index IS 'User notification index for sharding mode'; diff --git a/extension/storage/storage-mysql/src/main/resources/sql/sharding/schema-sharding-mysql.sql b/extension/storage/storage-mysql/src/main/resources/sql/sharding/schema-sharding-mysql.sql new file mode 100644 index 000000000..bc2a4e9b2 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/sharding/schema-sharding-mysql.sql @@ -0,0 +1,137 @@ +-- =========================================== +-- SmartEngine core tables - MySQL HASH partitioned version +-- All IDs generated by application-layer SnowflakeIdGenerator +-- se_deployment_instance is NOT partitioned and NOT included here +-- =========================================== + +-- =========================================== +-- Process instance table (root table) +-- Partition key: id (PK remains single-column id) +-- =========================================== +CREATE TABLE `se_process_instance` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_definition_id_and_version` varchar(128) NOT NULL COMMENT 'process definition id and version', + `process_definition_type` varchar(255) DEFAULT NULL COMMENT 'process definition type', + `status` varchar(64) NOT NULL COMMENT ' 1.running 2.completed 3.aborted', + `parent_process_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'parent process instance id', + `parent_execution_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'parent execution instance id', + `start_user_id` varchar(128) DEFAULT NULL COMMENT 'start user id', + `biz_unique_id` varchar(255) DEFAULT NULL COMMENT 'biz unique id', + `reason` varchar(255) DEFAULT NULL COMMENT 'reason', + `comment` varchar(255) DEFAULT NULL COMMENT 'comment', + `title` varchar(255) DEFAULT NULL COMMENT 'title', + `tag` varchar(255) DEFAULT NULL COMMENT 'tag', + `complete_time` datetime(6) DEFAULT NULL COMMENT 'process completion time', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Process instance table' +PARTITION BY HASH (`id`) PARTITIONS 16; + +-- =========================================== +-- Activity instance table +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE `se_activity_instance` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `process_definition_id_and_version` varchar(255) NOT NULL COMMENT 'process definition id and version', + `process_definition_activity_id` varchar(64) NOT NULL COMMENT 'process definition activity id', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`, `process_instance_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Activity instance table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; + +-- =========================================== +-- Execution instance table +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE `se_execution_instance` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `process_definition_id_and_version` varchar(255) NOT NULL COMMENT 'process definition id and version', + `process_definition_activity_id` varchar(255) NOT NULL COMMENT 'process definition activity id', + `activity_instance_id` bigint(20) unsigned NOT NULL COMMENT 'activity instance id', + `block_id` bigint(20) unsigned DEFAULT NULL COMMENT 'block_id', + `active` tinyint(4) NOT NULL COMMENT '1:active 0:inactive', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`, `process_instance_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Execution instance table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; + +-- =========================================== +-- Task instance table +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE `se_task_instance` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `process_definition_id_and_version` varchar(128) DEFAULT NULL COMMENT 'process definition id and version', + `process_definition_type` varchar(255) DEFAULT NULL COMMENT 'process definition type', + `activity_instance_id` bigint(20) unsigned NOT NULL COMMENT 'activity instance id', + `process_definition_activity_id` varchar(255) NOT NULL COMMENT 'process definition activity id', + `execution_instance_id` bigint(20) unsigned NOT NULL COMMENT 'execution instance id', + `claim_user_id` varchar(255) DEFAULT NULL COMMENT 'claim user id', + `title` varchar(255) DEFAULT NULL COMMENT 'title', + `priority` int(11) DEFAULT 500 COMMENT 'priority', + `tag` varchar(255) DEFAULT NULL COMMENT 'tag', + `claim_time` datetime(6) DEFAULT NULL COMMENT 'claim time', + `complete_time` datetime(6) DEFAULT NULL COMMENT 'complete time', + `status` varchar(255) NOT NULL COMMENT 'status', + `comment` varchar(255) DEFAULT NULL COMMENT 'comment', + `extension` varchar(255) DEFAULT NULL COMMENT 'extension', + `domain_code` varchar(64) DEFAULT NULL COMMENT 'domain code', + `extra` json DEFAULT NULL COMMENT 'extra JSON data', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`, `process_instance_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Task instance table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; + +-- =========================================== +-- Task assignee instance table +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE `se_task_assignee_instance` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `assignee_id` varchar(255) NOT NULL COMMENT 'assignee id', + `assignee_type` varchar(128) NOT NULL COMMENT 'assignee type', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`, `process_instance_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Task assignee instance table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; + +-- =========================================== +-- Variable instance table +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE `se_variable_instance` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `execution_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'execution instance id', + `field_key` varchar(128) NOT NULL COMMENT 'field key', + `field_type` varchar(128) NOT NULL COMMENT 'field type', + `field_double_value` decimal(65,30) DEFAULT NULL COMMENT 'field double value', + `field_long_value` bigint(20) DEFAULT NULL COMMENT 'field long value', + `field_string_value` varchar(4000) DEFAULT NULL COMMENT 'field string value', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`, `process_instance_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Variable instance table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; diff --git a/extension/storage/storage-mysql/src/main/resources/sql/sharding/schema-sharding-postgresql.sql b/extension/storage/storage-mysql/src/main/resources/sql/sharding/schema-sharding-postgresql.sql new file mode 100644 index 000000000..83d63979c --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/sharding/schema-sharding-postgresql.sql @@ -0,0 +1,248 @@ +-- =========================================== +-- SmartEngine core tables - PostgreSQL HASH partitioned version +-- All IDs generated by application-layer SnowflakeIdGenerator +-- se_deployment_instance is NOT partitioned and NOT included here +-- =========================================== + +-- =========================================== +-- Process instance table (root table) +-- Partition key: id (PK remains single-column id) +-- =========================================== +CREATE TABLE se_process_instance ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_definition_id_and_version varchar(128) NOT NULL, + process_definition_type varchar(255), + status varchar(64) NOT NULL, + parent_process_instance_id bigint, + parent_execution_instance_id bigint, + start_user_id varchar(128), + biz_unique_id varchar(255), + reason varchar(255), + comment varchar(255), + title varchar(255), + tag varchar(255), + complete_time timestamp(6), + tenant_id varchar(64), + PRIMARY KEY (id) +) PARTITION BY HASH (id); + +COMMENT ON TABLE se_process_instance IS 'Process instance table'; +COMMENT ON COLUMN se_process_instance.status IS '1.running 2.completed 3.aborted'; +COMMENT ON COLUMN se_process_instance.complete_time IS 'process completion time'; + +CREATE TABLE se_process_instance_p00 PARTITION OF se_process_instance FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_process_instance_p01 PARTITION OF se_process_instance FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_process_instance_p02 PARTITION OF se_process_instance FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_process_instance_p03 PARTITION OF se_process_instance FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_process_instance_p04 PARTITION OF se_process_instance FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_process_instance_p05 PARTITION OF se_process_instance FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_process_instance_p06 PARTITION OF se_process_instance FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_process_instance_p07 PARTITION OF se_process_instance FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_process_instance_p08 PARTITION OF se_process_instance FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_process_instance_p09 PARTITION OF se_process_instance FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_process_instance_p10 PARTITION OF se_process_instance FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_process_instance_p11 PARTITION OF se_process_instance FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_process_instance_p12 PARTITION OF se_process_instance FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_process_instance_p13 PARTITION OF se_process_instance FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_process_instance_p14 PARTITION OF se_process_instance FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_process_instance_p15 PARTITION OF se_process_instance FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +-- =========================================== +-- Activity instance table +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE se_activity_instance ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + process_definition_id_and_version varchar(255) NOT NULL, + process_definition_activity_id varchar(64) NOT NULL, + tenant_id varchar(64), + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +COMMENT ON TABLE se_activity_instance IS 'Activity instance table'; + +CREATE TABLE se_activity_instance_p00 PARTITION OF se_activity_instance FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_activity_instance_p01 PARTITION OF se_activity_instance FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_activity_instance_p02 PARTITION OF se_activity_instance FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_activity_instance_p03 PARTITION OF se_activity_instance FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_activity_instance_p04 PARTITION OF se_activity_instance FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_activity_instance_p05 PARTITION OF se_activity_instance FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_activity_instance_p06 PARTITION OF se_activity_instance FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_activity_instance_p07 PARTITION OF se_activity_instance FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_activity_instance_p08 PARTITION OF se_activity_instance FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_activity_instance_p09 PARTITION OF se_activity_instance FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_activity_instance_p10 PARTITION OF se_activity_instance FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_activity_instance_p11 PARTITION OF se_activity_instance FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_activity_instance_p12 PARTITION OF se_activity_instance FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_activity_instance_p13 PARTITION OF se_activity_instance FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_activity_instance_p14 PARTITION OF se_activity_instance FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_activity_instance_p15 PARTITION OF se_activity_instance FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +-- =========================================== +-- Execution instance table +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE se_execution_instance ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + process_definition_id_and_version varchar(255) NOT NULL, + process_definition_activity_id varchar(255) NOT NULL, + activity_instance_id bigint NOT NULL, + block_id bigint, + active smallint NOT NULL, + tenant_id varchar(64), + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +COMMENT ON TABLE se_execution_instance IS 'Execution instance table'; +COMMENT ON COLUMN se_execution_instance.active IS '1:active 0:inactive'; + +CREATE TABLE se_execution_instance_p00 PARTITION OF se_execution_instance FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_execution_instance_p01 PARTITION OF se_execution_instance FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_execution_instance_p02 PARTITION OF se_execution_instance FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_execution_instance_p03 PARTITION OF se_execution_instance FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_execution_instance_p04 PARTITION OF se_execution_instance FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_execution_instance_p05 PARTITION OF se_execution_instance FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_execution_instance_p06 PARTITION OF se_execution_instance FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_execution_instance_p07 PARTITION OF se_execution_instance FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_execution_instance_p08 PARTITION OF se_execution_instance FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_execution_instance_p09 PARTITION OF se_execution_instance FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_execution_instance_p10 PARTITION OF se_execution_instance FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_execution_instance_p11 PARTITION OF se_execution_instance FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_execution_instance_p12 PARTITION OF se_execution_instance FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_execution_instance_p13 PARTITION OF se_execution_instance FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_execution_instance_p14 PARTITION OF se_execution_instance FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_execution_instance_p15 PARTITION OF se_execution_instance FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +-- =========================================== +-- Task instance table +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE se_task_instance ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + process_definition_id_and_version varchar(128), + process_definition_type varchar(255), + activity_instance_id bigint NOT NULL, + process_definition_activity_id varchar(255) NOT NULL, + execution_instance_id bigint NOT NULL, + claim_user_id varchar(255), + title varchar(255), + priority int DEFAULT 500, + tag varchar(255), + claim_time timestamp(6), + complete_time timestamp(6), + status varchar(255) NOT NULL, + comment varchar(255), + extension varchar(255), + domain_code varchar(64) DEFAULT NULL, + extra jsonb DEFAULT NULL, + tenant_id varchar(64), + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +COMMENT ON TABLE se_task_instance IS 'Task instance table'; + +CREATE TABLE se_task_instance_p00 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_task_instance_p01 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_task_instance_p02 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_task_instance_p03 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_task_instance_p04 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_task_instance_p05 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_task_instance_p06 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_task_instance_p07 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_task_instance_p08 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_task_instance_p09 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_task_instance_p10 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_task_instance_p11 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_task_instance_p12 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_task_instance_p13 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_task_instance_p14 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_task_instance_p15 PARTITION OF se_task_instance FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +-- =========================================== +-- Task assignee instance table +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE se_task_assignee_instance ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint NOT NULL, + assignee_id varchar(255) NOT NULL, + assignee_type varchar(128) NOT NULL, + tenant_id varchar(64), + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +COMMENT ON TABLE se_task_assignee_instance IS 'Task assignee instance table'; + +CREATE TABLE se_task_assignee_instance_p00 PARTITION OF se_task_assignee_instance FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_task_assignee_instance_p01 PARTITION OF se_task_assignee_instance FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_task_assignee_instance_p02 PARTITION OF se_task_assignee_instance FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_task_assignee_instance_p03 PARTITION OF se_task_assignee_instance FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_task_assignee_instance_p04 PARTITION OF se_task_assignee_instance FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_task_assignee_instance_p05 PARTITION OF se_task_assignee_instance FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_task_assignee_instance_p06 PARTITION OF se_task_assignee_instance FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_task_assignee_instance_p07 PARTITION OF se_task_assignee_instance FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_task_assignee_instance_p08 PARTITION OF se_task_assignee_instance FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_task_assignee_instance_p09 PARTITION OF se_task_assignee_instance FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_task_assignee_instance_p10 PARTITION OF se_task_assignee_instance FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_task_assignee_instance_p11 PARTITION OF se_task_assignee_instance FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_task_assignee_instance_p12 PARTITION OF se_task_assignee_instance FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_task_assignee_instance_p13 PARTITION OF se_task_assignee_instance FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_task_assignee_instance_p14 PARTITION OF se_task_assignee_instance FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_task_assignee_instance_p15 PARTITION OF se_task_assignee_instance FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +-- =========================================== +-- Variable instance table +-- Partition key: process_instance_id +-- PK: (id, process_instance_id) +-- =========================================== +CREATE TABLE se_variable_instance ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + execution_instance_id bigint, + field_key varchar(128) NOT NULL, + field_type varchar(128) NOT NULL, + field_double_value decimal(65,30), + field_long_value bigint, + field_string_value varchar(4000), + tenant_id varchar(64), + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +COMMENT ON TABLE se_variable_instance IS 'Variable instance table'; + +CREATE TABLE se_variable_instance_p00 PARTITION OF se_variable_instance FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_variable_instance_p01 PARTITION OF se_variable_instance FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_variable_instance_p02 PARTITION OF se_variable_instance FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_variable_instance_p03 PARTITION OF se_variable_instance FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_variable_instance_p04 PARTITION OF se_variable_instance FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_variable_instance_p05 PARTITION OF se_variable_instance FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_variable_instance_p06 PARTITION OF se_variable_instance FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_variable_instance_p07 PARTITION OF se_variable_instance FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_variable_instance_p08 PARTITION OF se_variable_instance FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_variable_instance_p09 PARTITION OF se_variable_instance FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_variable_instance_p10 PARTITION OF se_variable_instance FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_variable_instance_p11 PARTITION OF se_variable_instance FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_variable_instance_p12 PARTITION OF se_variable_instance FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_variable_instance_p13 PARTITION OF se_variable_instance FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_variable_instance_p14 PARTITION OF se_variable_instance FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_variable_instance_p15 PARTITION OF se_variable_instance FOR VALUES WITH (MODULUS 16, REMAINDER 15); diff --git a/extension/storage/storage-mysql/src/main/resources/sql/sharding/verify-partition-pruning-mysql.sql b/extension/storage/storage-mysql/src/main/resources/sql/sharding/verify-partition-pruning-mysql.sql new file mode 100644 index 000000000..4aa0310ae --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/sharding/verify-partition-pruning-mysql.sql @@ -0,0 +1,85 @@ +-- =========================================== +-- Partition Pruning Verification Scripts (MySQL) +-- Run these after deploying the sharding schema +-- Each query should show "partitions: pN" (single partition) in the plan +-- =========================================== + +-- ============ 1. Process instance by ID ============ +-- Expected: partitions shows only 1 partition +EXPLAIN +SELECT * FROM se_process_instance WHERE id = 123456; + +-- ============ 2. Task instances by process_instance_id ============ +-- Expected: partitions shows only 1 partition +EXPLAIN +SELECT * FROM se_task_instance WHERE process_instance_id = 123456; + +-- ============ 3. Execution instances by process_instance_id ============ +-- Expected: partitions shows only 1 partition +EXPLAIN +SELECT * FROM se_execution_instance +WHERE process_instance_id = 123456 AND active = 1; + +-- ============ 4. Activity instances by process_instance_id ============ +EXPLAIN +SELECT * FROM se_activity_instance WHERE process_instance_id = 123456; + +-- ============ 5. Task assignees by process_instance_id ============ +EXPLAIN +SELECT * FROM se_task_assignee_instance WHERE process_instance_id = 123456; + +-- ============ 6. Variables by process_instance_id ============ +EXPLAIN +SELECT * FROM se_variable_instance WHERE process_instance_id = 123456; + +-- ============ 7. Supervision by process_instance_id ============ +EXPLAIN +SELECT * FROM se_supervision_instance WHERE process_instance_id = 123456; + +-- ============ 8. Notifications by process_instance_id ============ +EXPLAIN +SELECT * FROM se_notification_instance WHERE process_instance_id = 123456; + +-- ============ 9. Transfer records by process_instance_id ============ +EXPLAIN +SELECT * FROM se_task_transfer_record WHERE process_instance_id = 123456; + +-- ============ 10. Operation records by process_instance_id ============ +EXPLAIN +SELECT * FROM se_assignee_operation_record WHERE process_instance_id = 123456; + +-- ============ 11. Rollback records by process_instance_id ============ +EXPLAIN +SELECT * FROM se_process_rollback_record WHERE process_instance_id = 123456; + +-- =========================================== +-- Index table queries (NOT partitioned) +-- =========================================== + +-- ============ 12. User task index ============ +-- Expected: Using index idx_user_task_pending +EXPLAIN +SELECT * FROM se_user_task_index +WHERE tenant_id = 'test' AND assignee_id = 'user001' AND task_status = 'pending'; + +-- ============ 13. User notification index ============ +-- Expected: Using index idx_user_notif_receiver +EXPLAIN +SELECT * FROM se_user_notification_index +WHERE tenant_id = 'test' AND receiver_user_id = 'user001' AND read_status = 'unread'; + +-- =========================================== +-- Anti-pattern: Query without partition key +-- =========================================== + +-- ============ 14. Task by ID only (no process_instance_id) ============ +-- Expected: partitions shows ALL partitions (p0,p1,...,p15) +EXPLAIN +SELECT * FROM se_task_instance WHERE id = 123456; + +-- =========================================== +-- Verification Checklist +-- =========================================== +-- [ ] Queries 1-11: "partitions" column shows single partition (e.g. "p8") +-- [ ] Query 12-13: "key" column shows the index name +-- [ ] Query 14: "partitions" shows all 16 (p0,p1,...,p15) diff --git a/extension/storage/storage-mysql/src/main/resources/sql/sharding/verify-partition-pruning-postgresql.sql b/extension/storage/storage-mysql/src/main/resources/sql/sharding/verify-partition-pruning-postgresql.sql new file mode 100644 index 000000000..b15eb7c12 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/sharding/verify-partition-pruning-postgresql.sql @@ -0,0 +1,107 @@ +-- =========================================== +-- Partition Pruning Verification Scripts (PostgreSQL) +-- Run these after deploying the sharding schema +-- Each query should show "Scans: 1 of 16" in the plan +-- =========================================== + +-- ============ 1. Process instance by ID ============ +-- Expected: Only scan 1 partition +EXPLAIN ANALYZE +SELECT * FROM se_process_instance WHERE id = 123456; + +-- ============ 2. Task instances by process_instance_id ============ +-- Expected: Only scan 1 partition (co-located with process) +EXPLAIN ANALYZE +SELECT * FROM se_task_instance WHERE process_instance_id = 123456; + +-- ============ 3. Execution instances by process_instance_id ============ +-- Expected: Only scan 1 partition +EXPLAIN ANALYZE +SELECT * FROM se_execution_instance +WHERE process_instance_id = 123456 AND active = 1; + +-- ============ 4. Activity instances by process_instance_id ============ +-- Expected: Only scan 1 partition +EXPLAIN ANALYZE +SELECT * FROM se_activity_instance WHERE process_instance_id = 123456; + +-- ============ 5. Task assignees by process_instance_id ============ +-- Expected: Only scan 1 partition +EXPLAIN ANALYZE +SELECT * FROM se_task_assignee_instance WHERE process_instance_id = 123456; + +-- ============ 6. Variables by process_instance_id ============ +-- Expected: Only scan 1 partition +EXPLAIN ANALYZE +SELECT * FROM se_variable_instance WHERE process_instance_id = 123456; + +-- ============ 7. Supervision by process_instance_id ============ +-- Expected: Only scan 1 partition +EXPLAIN ANALYZE +SELECT * FROM se_supervision_instance WHERE process_instance_id = 123456; + +-- ============ 8. Notifications by process_instance_id ============ +-- Expected: Only scan 1 partition +EXPLAIN ANALYZE +SELECT * FROM se_notification_instance WHERE process_instance_id = 123456; + +-- ============ 9. Transfer records by process_instance_id ============ +-- Expected: Only scan 1 partition +EXPLAIN ANALYZE +SELECT * FROM se_task_transfer_record WHERE process_instance_id = 123456; + +-- ============ 10. Operation records by process_instance_id ============ +-- Expected: Only scan 1 partition +EXPLAIN ANALYZE +SELECT * FROM se_assignee_operation_record WHERE process_instance_id = 123456; + +-- ============ 11. Rollback records by process_instance_id ============ +-- Expected: Only scan 1 partition +EXPLAIN ANALYZE +SELECT * FROM se_process_rollback_record WHERE process_instance_id = 123456; + +-- =========================================== +-- Cross-partition queries (expected to scan all 16 partitions) +-- These use the index tables instead +-- =========================================== + +-- ============ 12. User task index (NOT partitioned) ============ +-- Expected: Index scan on uk_user_task_idx or idx_user_task_pending +EXPLAIN ANALYZE +SELECT * FROM se_user_task_index +WHERE tenant_id = 'test' AND assignee_id = 'user001' AND task_status = 'pending'; + +-- ============ 13. User notification index (NOT partitioned) ============ +-- Expected: Index scan on idx_user_notif_unread (partial index) +EXPLAIN ANALYZE +SELECT * FROM se_user_notification_index +WHERE tenant_id = 'test' AND receiver_user_id = 'user001' AND read_status = 'unread'; + +-- =========================================== +-- Archive table partition pruning +-- =========================================== + +-- ============ 14. Archive process by ID ============ +EXPLAIN ANALYZE +SELECT * FROM se_process_instance_archive WHERE id = 123456; + +-- ============ 15. Archive tasks by process_instance_id ============ +EXPLAIN ANALYZE +SELECT * FROM se_task_instance_archive WHERE process_instance_id = 123456; + +-- =========================================== +-- Anti-pattern: Query without partition key (should scan all partitions) +-- =========================================== + +-- ============ 16. Task by ID only (no process_instance_id) ============ +-- Expected: Scan all 16 partitions — this is the anti-pattern +EXPLAIN ANALYZE +SELECT * FROM se_task_instance WHERE id = 123456; + +-- =========================================== +-- Verification Checklist +-- =========================================== +-- [ ] Queries 1-11: Each should show "Scans: 1 of 16" or only 1 partition table +-- [ ] Query 12-13: Should show Index Scan (not Seq Scan) +-- [ ] Query 14-15: Each should show only 1 partition scanned +-- [ ] Query 16: Should show all 16 partitions scanned (expected anti-pattern) diff --git a/extension/storage/storage-mysql/src/main/resources/sql/sharding/workflow-enhancement-sharding-mysql.sql b/extension/storage/storage-mysql/src/main/resources/sql/sharding/workflow-enhancement-sharding-mysql.sql new file mode 100644 index 000000000..1bb65191d --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/sharding/workflow-enhancement-sharding-mysql.sql @@ -0,0 +1,119 @@ +-- =========================================== +-- Workflow enhancement tables - MySQL HASH partitioned version +-- All IDs generated by application-layer SnowflakeIdGenerator +-- All tables partitioned by process_instance_id with 16 partitions +-- PK: (id, process_instance_id) +-- =========================================== + +-- =========================================== +-- Supervision instance table +-- =========================================== +CREATE TABLE `se_supervision_instance` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `supervisor_user_id` varchar(255) NOT NULL COMMENT 'supervisor user id', + `supervision_reason` varchar(500) DEFAULT NULL COMMENT 'supervision reason', + `supervision_type` varchar(64) NOT NULL COMMENT 'supervision type: urge/track/remind', + `status` varchar(64) NOT NULL COMMENT 'status: active/closed', + `close_time` datetime(6) DEFAULT NULL COMMENT 'close time', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`, `process_instance_id`), + KEY `idx_task_status_tenant` (`task_instance_id`, `status`, `tenant_id`), + KEY `idx_supervisor_tenant` (`supervisor_user_id`, `tenant_id`), + KEY `idx_process_instance_id` (`process_instance_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Supervision instance table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; + +-- =========================================== +-- Notification instance table +-- =========================================== +CREATE TABLE `se_notification_instance` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'task instance id', + `sender_user_id` varchar(255) NOT NULL COMMENT 'sender user id', + `receiver_user_id` varchar(255) NOT NULL COMMENT 'receiver user id', + `notification_type` varchar(64) NOT NULL COMMENT 'notification type: cc/inform', + `title` varchar(255) DEFAULT NULL COMMENT 'notification title', + `content` varchar(1000) DEFAULT NULL COMMENT 'notification content', + `read_status` varchar(64) NOT NULL DEFAULT 'unread' COMMENT 'read status: unread/read', + `read_time` datetime(6) DEFAULT NULL COMMENT 'read time', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`, `process_instance_id`), + KEY `idx_receiver_read_tenant` (`receiver_user_id`, `read_status`, `tenant_id`), + KEY `idx_sender_tenant` (`sender_user_id`, `tenant_id`), + KEY `idx_process_instance_id` (`process_instance_id`), + KEY `idx_task_instance_id` (`task_instance_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Notification instance table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; + +-- =========================================== +-- Task transfer record table +-- =========================================== +CREATE TABLE `se_task_transfer_record` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `from_user_id` varchar(255) NOT NULL COMMENT 'from user id', + `to_user_id` varchar(255) NOT NULL COMMENT 'to user id', + `transfer_reason` varchar(500) DEFAULT NULL COMMENT 'transfer reason', + `deadline` datetime(6) DEFAULT NULL COMMENT 'processing deadline', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`, `process_instance_id`), + KEY `idx_task_tenant` (`task_instance_id`, `tenant_id`), + KEY `idx_from_user_id` (`from_user_id`), + KEY `idx_to_user_id` (`to_user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Task transfer record table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; + +-- =========================================== +-- Assignee operation record table +-- =========================================== +CREATE TABLE `se_assignee_operation_record` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `operation_type` varchar(64) NOT NULL COMMENT 'operation type: add_assignee/remove_assignee', + `operator_user_id` varchar(255) NOT NULL COMMENT 'operator user id', + `target_user_id` varchar(255) NOT NULL COMMENT 'target user id', + `operation_reason` varchar(500) DEFAULT NULL COMMENT 'operation reason', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`, `process_instance_id`), + KEY `idx_task_tenant` (`task_instance_id`, `tenant_id`), + KEY `idx_operation_type` (`operation_type`), + KEY `idx_operator_user_id` (`operator_user_id`), + KEY `idx_target_user_id` (`target_user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Assignee operation record table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; + +-- =========================================== +-- Process rollback record table +-- =========================================== +CREATE TABLE `se_process_rollback_record` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `rollback_type` varchar(64) NOT NULL COMMENT 'rollback type: previous/specific', + `from_activity_id` varchar(255) NOT NULL COMMENT 'from activity id', + `to_activity_id` varchar(255) NOT NULL COMMENT 'to activity id', + `operator_user_id` varchar(255) NOT NULL COMMENT 'operator user id', + `rollback_reason` varchar(500) DEFAULT NULL COMMENT 'rollback reason', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`, `process_instance_id`), + KEY `idx_process_tenant` (`process_instance_id`, `tenant_id`), + KEY `idx_task_instance_id` (`task_instance_id`), + KEY `idx_rollback_type` (`rollback_type`), + KEY `idx_operator_user_id` (`operator_user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Process rollback record table' +PARTITION BY HASH (`process_instance_id`) PARTITIONS 16; diff --git a/extension/storage/storage-mysql/src/main/resources/sql/sharding/workflow-enhancement-sharding-postgresql.sql b/extension/storage/storage-mysql/src/main/resources/sql/sharding/workflow-enhancement-sharding-postgresql.sql new file mode 100644 index 000000000..a1721d269 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/sharding/workflow-enhancement-sharding-postgresql.sql @@ -0,0 +1,238 @@ +-- =========================================== +-- Workflow enhancement tables - PostgreSQL HASH partitioned version +-- All IDs generated by application-layer SnowflakeIdGenerator +-- All tables partitioned by process_instance_id with 16 partitions +-- PK: (id, process_instance_id) +-- =========================================== + +-- =========================================== +-- Supervision instance table +-- =========================================== +CREATE TABLE se_supervision_instance ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint NOT NULL, + supervisor_user_id varchar(255) NOT NULL, + supervision_reason varchar(500), + supervision_type varchar(64) NOT NULL, + status varchar(64) NOT NULL, + close_time timestamp(6), + tenant_id varchar(64), + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +COMMENT ON TABLE se_supervision_instance IS 'Supervision instance table'; +COMMENT ON COLUMN se_supervision_instance.supervision_type IS 'urge/track/remind'; +COMMENT ON COLUMN se_supervision_instance.status IS 'active/closed'; + +CREATE INDEX idx_supervision_task_status_tenant + ON se_supervision_instance (task_instance_id, status, tenant_id); +CREATE INDEX idx_supervision_supervisor_tenant + ON se_supervision_instance (supervisor_user_id, tenant_id); +CREATE INDEX idx_supervision_process_instance_id + ON se_supervision_instance (process_instance_id); + +CREATE TABLE se_supervision_instance_p00 PARTITION OF se_supervision_instance FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_supervision_instance_p01 PARTITION OF se_supervision_instance FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_supervision_instance_p02 PARTITION OF se_supervision_instance FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_supervision_instance_p03 PARTITION OF se_supervision_instance FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_supervision_instance_p04 PARTITION OF se_supervision_instance FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_supervision_instance_p05 PARTITION OF se_supervision_instance FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_supervision_instance_p06 PARTITION OF se_supervision_instance FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_supervision_instance_p07 PARTITION OF se_supervision_instance FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_supervision_instance_p08 PARTITION OF se_supervision_instance FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_supervision_instance_p09 PARTITION OF se_supervision_instance FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_supervision_instance_p10 PARTITION OF se_supervision_instance FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_supervision_instance_p11 PARTITION OF se_supervision_instance FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_supervision_instance_p12 PARTITION OF se_supervision_instance FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_supervision_instance_p13 PARTITION OF se_supervision_instance FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_supervision_instance_p14 PARTITION OF se_supervision_instance FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_supervision_instance_p15 PARTITION OF se_supervision_instance FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +-- =========================================== +-- Notification instance table +-- =========================================== +CREATE TABLE se_notification_instance ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint, + sender_user_id varchar(255) NOT NULL, + receiver_user_id varchar(255) NOT NULL, + notification_type varchar(64) NOT NULL, + title varchar(255), + content varchar(1000), + read_status varchar(64) NOT NULL DEFAULT 'unread', + read_time timestamp(6), + tenant_id varchar(64), + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +COMMENT ON TABLE se_notification_instance IS 'Notification instance table'; +COMMENT ON COLUMN se_notification_instance.notification_type IS 'cc/inform'; +COMMENT ON COLUMN se_notification_instance.read_status IS 'unread/read'; + +CREATE INDEX idx_notification_receiver_read_tenant + ON se_notification_instance (receiver_user_id, read_status, tenant_id); +CREATE INDEX idx_notification_sender_tenant + ON se_notification_instance (sender_user_id, tenant_id); +CREATE INDEX idx_notification_process_instance_id + ON se_notification_instance (process_instance_id); +CREATE INDEX idx_notification_task_instance_id + ON se_notification_instance (task_instance_id); + +CREATE TABLE se_notification_instance_p00 PARTITION OF se_notification_instance FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_notification_instance_p01 PARTITION OF se_notification_instance FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_notification_instance_p02 PARTITION OF se_notification_instance FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_notification_instance_p03 PARTITION OF se_notification_instance FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_notification_instance_p04 PARTITION OF se_notification_instance FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_notification_instance_p05 PARTITION OF se_notification_instance FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_notification_instance_p06 PARTITION OF se_notification_instance FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_notification_instance_p07 PARTITION OF se_notification_instance FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_notification_instance_p08 PARTITION OF se_notification_instance FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_notification_instance_p09 PARTITION OF se_notification_instance FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_notification_instance_p10 PARTITION OF se_notification_instance FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_notification_instance_p11 PARTITION OF se_notification_instance FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_notification_instance_p12 PARTITION OF se_notification_instance FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_notification_instance_p13 PARTITION OF se_notification_instance FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_notification_instance_p14 PARTITION OF se_notification_instance FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_notification_instance_p15 PARTITION OF se_notification_instance FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +-- =========================================== +-- Task transfer record table +-- =========================================== +CREATE TABLE se_task_transfer_record ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + task_instance_id bigint NOT NULL, + process_instance_id bigint NOT NULL, + from_user_id varchar(255) NOT NULL, + to_user_id varchar(255) NOT NULL, + transfer_reason varchar(500), + deadline timestamp(6), + tenant_id varchar(64), + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +COMMENT ON TABLE se_task_transfer_record IS 'Task transfer record table'; + +CREATE INDEX idx_task_transfer_task_tenant + ON se_task_transfer_record (task_instance_id, tenant_id); +CREATE INDEX idx_task_transfer_from_user_id + ON se_task_transfer_record (from_user_id); +CREATE INDEX idx_task_transfer_to_user_id + ON se_task_transfer_record (to_user_id); + +CREATE TABLE se_task_transfer_record_p00 PARTITION OF se_task_transfer_record FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_task_transfer_record_p01 PARTITION OF se_task_transfer_record FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_task_transfer_record_p02 PARTITION OF se_task_transfer_record FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_task_transfer_record_p03 PARTITION OF se_task_transfer_record FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_task_transfer_record_p04 PARTITION OF se_task_transfer_record FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_task_transfer_record_p05 PARTITION OF se_task_transfer_record FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_task_transfer_record_p06 PARTITION OF se_task_transfer_record FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_task_transfer_record_p07 PARTITION OF se_task_transfer_record FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_task_transfer_record_p08 PARTITION OF se_task_transfer_record FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_task_transfer_record_p09 PARTITION OF se_task_transfer_record FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_task_transfer_record_p10 PARTITION OF se_task_transfer_record FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_task_transfer_record_p11 PARTITION OF se_task_transfer_record FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_task_transfer_record_p12 PARTITION OF se_task_transfer_record FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_task_transfer_record_p13 PARTITION OF se_task_transfer_record FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_task_transfer_record_p14 PARTITION OF se_task_transfer_record FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_task_transfer_record_p15 PARTITION OF se_task_transfer_record FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +-- =========================================== +-- Assignee operation record table +-- =========================================== +CREATE TABLE se_assignee_operation_record ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + task_instance_id bigint NOT NULL, + process_instance_id bigint NOT NULL, + operation_type varchar(64) NOT NULL, + operator_user_id varchar(255) NOT NULL, + target_user_id varchar(255) NOT NULL, + operation_reason varchar(500), + tenant_id varchar(64), + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +COMMENT ON TABLE se_assignee_operation_record IS 'Assignee operation record table'; +COMMENT ON COLUMN se_assignee_operation_record.operation_type IS 'add_assignee/remove_assignee'; + +CREATE INDEX idx_assignee_op_task_tenant + ON se_assignee_operation_record (task_instance_id, tenant_id); +CREATE INDEX idx_assignee_op_operation_type + ON se_assignee_operation_record (operation_type); +CREATE INDEX idx_assignee_op_operator_user_id + ON se_assignee_operation_record (operator_user_id); +CREATE INDEX idx_assignee_op_target_user_id + ON se_assignee_operation_record (target_user_id); + +CREATE TABLE se_assignee_operation_record_p00 PARTITION OF se_assignee_operation_record FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_assignee_operation_record_p01 PARTITION OF se_assignee_operation_record FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_assignee_operation_record_p02 PARTITION OF se_assignee_operation_record FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_assignee_operation_record_p03 PARTITION OF se_assignee_operation_record FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_assignee_operation_record_p04 PARTITION OF se_assignee_operation_record FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_assignee_operation_record_p05 PARTITION OF se_assignee_operation_record FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_assignee_operation_record_p06 PARTITION OF se_assignee_operation_record FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_assignee_operation_record_p07 PARTITION OF se_assignee_operation_record FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_assignee_operation_record_p08 PARTITION OF se_assignee_operation_record FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_assignee_operation_record_p09 PARTITION OF se_assignee_operation_record FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_assignee_operation_record_p10 PARTITION OF se_assignee_operation_record FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_assignee_operation_record_p11 PARTITION OF se_assignee_operation_record FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_assignee_operation_record_p12 PARTITION OF se_assignee_operation_record FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_assignee_operation_record_p13 PARTITION OF se_assignee_operation_record FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_assignee_operation_record_p14 PARTITION OF se_assignee_operation_record FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_assignee_operation_record_p15 PARTITION OF se_assignee_operation_record FOR VALUES WITH (MODULUS 16, REMAINDER 15); + +-- =========================================== +-- Process rollback record table +-- =========================================== +CREATE TABLE se_process_rollback_record ( + id bigint NOT NULL, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint NOT NULL, + rollback_type varchar(64) NOT NULL, + from_activity_id varchar(255) NOT NULL, + to_activity_id varchar(255) NOT NULL, + operator_user_id varchar(255) NOT NULL, + rollback_reason varchar(500), + tenant_id varchar(64), + PRIMARY KEY (id, process_instance_id) +) PARTITION BY HASH (process_instance_id); + +COMMENT ON TABLE se_process_rollback_record IS 'Process rollback record table'; +COMMENT ON COLUMN se_process_rollback_record.rollback_type IS 'previous/specific'; + +CREATE INDEX idx_rollback_process_tenant + ON se_process_rollback_record (process_instance_id, tenant_id); +CREATE INDEX idx_rollback_task_instance_id + ON se_process_rollback_record (task_instance_id); +CREATE INDEX idx_rollback_type + ON se_process_rollback_record (rollback_type); +CREATE INDEX idx_rollback_operator_user_id + ON se_process_rollback_record (operator_user_id); + +CREATE TABLE se_process_rollback_record_p00 PARTITION OF se_process_rollback_record FOR VALUES WITH (MODULUS 16, REMAINDER 0); +CREATE TABLE se_process_rollback_record_p01 PARTITION OF se_process_rollback_record FOR VALUES WITH (MODULUS 16, REMAINDER 1); +CREATE TABLE se_process_rollback_record_p02 PARTITION OF se_process_rollback_record FOR VALUES WITH (MODULUS 16, REMAINDER 2); +CREATE TABLE se_process_rollback_record_p03 PARTITION OF se_process_rollback_record FOR VALUES WITH (MODULUS 16, REMAINDER 3); +CREATE TABLE se_process_rollback_record_p04 PARTITION OF se_process_rollback_record FOR VALUES WITH (MODULUS 16, REMAINDER 4); +CREATE TABLE se_process_rollback_record_p05 PARTITION OF se_process_rollback_record FOR VALUES WITH (MODULUS 16, REMAINDER 5); +CREATE TABLE se_process_rollback_record_p06 PARTITION OF se_process_rollback_record FOR VALUES WITH (MODULUS 16, REMAINDER 6); +CREATE TABLE se_process_rollback_record_p07 PARTITION OF se_process_rollback_record FOR VALUES WITH (MODULUS 16, REMAINDER 7); +CREATE TABLE se_process_rollback_record_p08 PARTITION OF se_process_rollback_record FOR VALUES WITH (MODULUS 16, REMAINDER 8); +CREATE TABLE se_process_rollback_record_p09 PARTITION OF se_process_rollback_record FOR VALUES WITH (MODULUS 16, REMAINDER 9); +CREATE TABLE se_process_rollback_record_p10 PARTITION OF se_process_rollback_record FOR VALUES WITH (MODULUS 16, REMAINDER 10); +CREATE TABLE se_process_rollback_record_p11 PARTITION OF se_process_rollback_record FOR VALUES WITH (MODULUS 16, REMAINDER 11); +CREATE TABLE se_process_rollback_record_p12 PARTITION OF se_process_rollback_record FOR VALUES WITH (MODULUS 16, REMAINDER 12); +CREATE TABLE se_process_rollback_record_p13 PARTITION OF se_process_rollback_record FOR VALUES WITH (MODULUS 16, REMAINDER 13); +CREATE TABLE se_process_rollback_record_p14 PARTITION OF se_process_rollback_record FOR VALUES WITH (MODULUS 16, REMAINDER 14); +CREATE TABLE se_process_rollback_record_p15 PARTITION OF se_process_rollback_record FOR VALUES WITH (MODULUS 16, REMAINDER 15); diff --git a/extension/storage/storage-mysql/src/main/resources/sql/workflow-enhancement-schema-mysql.sql b/extension/storage/storage-mysql/src/main/resources/sql/workflow-enhancement-schema-mysql.sql new file mode 100644 index 000000000..6d5680963 --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/workflow-enhancement-schema-mysql.sql @@ -0,0 +1,122 @@ +-- Workflow enhancement database schema for MySQL +-- Based on SmartEngine existing table structure design + +-- =========================================== +-- Supervision instance table +-- =========================================== +CREATE TABLE `se_supervision_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `supervisor_user_id` varchar(255) NOT NULL COMMENT 'supervisor user id', + `supervision_reason` varchar(500) DEFAULT NULL COMMENT 'supervision reason', + `supervision_type` varchar(64) NOT NULL COMMENT 'supervision type: urge/track/remind', + `status` varchar(64) NOT NULL COMMENT 'status: active/closed', + `close_time` datetime(6) DEFAULT NULL COMMENT 'close time', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`), + -- Composite index for findActiveByTaskId (high frequency query) + KEY `idx_task_status_tenant` (`task_instance_id`, `status`, `tenant_id`), + -- Composite index for findBySupervisor + KEY `idx_supervisor_tenant` (`supervisor_user_id`, `tenant_id`), + -- Single column index for cross-process queries + KEY `idx_process_instance_id` (`process_instance_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Supervision instance table'; + +-- =========================================== +-- Notification instance table +-- =========================================== +CREATE TABLE `se_notification_instance` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'task instance id', + `sender_user_id` varchar(255) NOT NULL COMMENT 'sender user id', + `receiver_user_id` varchar(255) NOT NULL COMMENT 'receiver user id', + `notification_type` varchar(64) NOT NULL COMMENT 'notification type: cc/inform', + `title` varchar(255) DEFAULT NULL COMMENT 'notification title', + `content` varchar(1000) DEFAULT NULL COMMENT 'notification content', + `read_status` varchar(64) NOT NULL DEFAULT 'unread' COMMENT 'read status: unread/read', + `read_time` datetime(6) DEFAULT NULL COMMENT 'read time', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`), + -- Composite index for findByReceiver (highest frequency query - user unread messages) + KEY `idx_receiver_read_tenant` (`receiver_user_id`, `read_status`, `tenant_id`), + -- Composite index for findBySender + KEY `idx_sender_tenant` (`sender_user_id`, `tenant_id`), + -- Single column indexes for cross-entity queries + KEY `idx_process_instance_id` (`process_instance_id`), + KEY `idx_task_instance_id` (`task_instance_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Notification instance table'; + +-- =========================================== +-- Task transfer record table +-- =========================================== +CREATE TABLE `se_task_transfer_record` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `process_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'process instance id', + `from_user_id` varchar(255) NOT NULL COMMENT 'from user id', + `to_user_id` varchar(255) NOT NULL COMMENT 'to user id', + `transfer_reason` varchar(500) DEFAULT NULL COMMENT 'transfer reason', + `deadline` datetime(6) DEFAULT NULL COMMENT 'processing deadline', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`), + -- Composite index for selectByTaskInstanceId + KEY `idx_task_tenant` (`task_instance_id`, `tenant_id`), + -- Single column indexes for user queries + KEY `idx_from_user_id` (`from_user_id`), + KEY `idx_to_user_id` (`to_user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Task transfer record table'; + +-- =========================================== +-- Assignee operation record table +-- =========================================== +CREATE TABLE `se_assignee_operation_record` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `process_instance_id` bigint(20) unsigned DEFAULT NULL COMMENT 'process instance id', + `operation_type` varchar(64) NOT NULL COMMENT 'operation type: add_assignee/remove_assignee', + `operator_user_id` varchar(255) NOT NULL COMMENT 'operator user id', + `target_user_id` varchar(255) NOT NULL COMMENT 'target user id', + `operation_reason` varchar(500) DEFAULT NULL COMMENT 'operation reason', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`), + -- Composite index for selectByTaskInstanceId + KEY `idx_task_tenant` (`task_instance_id`, `tenant_id`), + -- Single column indexes for audit queries + KEY `idx_operation_type` (`operation_type`), + KEY `idx_operator_user_id` (`operator_user_id`), + KEY `idx_target_user_id` (`target_user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Assignee operation record table'; + +-- =========================================== +-- Process rollback record table +-- =========================================== +CREATE TABLE `se_process_rollback_record` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK', + `gmt_create` datetime(6) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(6) NOT NULL COMMENT 'modification time', + `process_instance_id` bigint(20) unsigned NOT NULL COMMENT 'process instance id', + `task_instance_id` bigint(20) unsigned NOT NULL COMMENT 'task instance id', + `rollback_type` varchar(64) NOT NULL COMMENT 'rollback type: previous/specific', + `from_activity_id` varchar(255) NOT NULL COMMENT 'from activity id', + `to_activity_id` varchar(255) NOT NULL COMMENT 'to activity id', + `operator_user_id` varchar(255) NOT NULL COMMENT 'operator user id', + `rollback_reason` varchar(500) DEFAULT NULL COMMENT 'rollback reason', + `tenant_id` varchar(64) DEFAULT NULL COMMENT 'tenant id', + PRIMARY KEY (`id`), + -- Composite index for selectByProcessInstanceId + KEY `idx_process_tenant` (`process_instance_id`, `tenant_id`), + -- Single column indexes for audit queries + KEY `idx_task_instance_id` (`task_instance_id`), + KEY `idx_rollback_type` (`rollback_type`), + KEY `idx_operator_user_id` (`operator_user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Process rollback record table'; diff --git a/extension/storage/storage-mysql/src/main/resources/sql/workflow-enhancement-schema-oracle.sql b/extension/storage/storage-mysql/src/main/resources/sql/workflow-enhancement-schema-oracle.sql new file mode 100644 index 000000000..9c800c4be --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/workflow-enhancement-schema-oracle.sql @@ -0,0 +1,163 @@ +-- Workflow enhancement database schema for Oracle/DM (DaMeng) +-- Compatible with both Oracle Database and DaMeng Database +-- Based on SmartEngine existing table structure design + +-- =========================================== +-- Sequences +-- =========================================== +CREATE SEQUENCE se_supervision_instance_seq START WITH 1 INCREMENT BY 1; +CREATE SEQUENCE se_notification_instance_seq START WITH 1 INCREMENT BY 1; +CREATE SEQUENCE se_task_transfer_record_seq START WITH 1 INCREMENT BY 1; +CREATE SEQUENCE se_assignee_operation_record_seq START WITH 1 INCREMENT BY 1; +CREATE SEQUENCE se_process_rollback_record_seq START WITH 1 INCREMENT BY 1; + +-- =========================================== +-- Supervision instance table +-- =========================================== +CREATE TABLE se_supervision_instance ( + id NUMBER(19) DEFAULT se_supervision_instance_seq.NEXTVAL NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_instance_id NUMBER(19) NOT NULL, + task_instance_id NUMBER(19) NOT NULL, + supervisor_user_id VARCHAR2(255) NOT NULL, + supervision_reason VARCHAR2(500) DEFAULT NULL, + supervision_type VARCHAR2(64) NOT NULL, + status VARCHAR2(64) NOT NULL, + close_time TIMESTAMP(6) DEFAULT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + CONSTRAINT pk_supervision_instance PRIMARY KEY (id) +); + +COMMENT ON TABLE se_supervision_instance IS 'Supervision instance table'; +COMMENT ON COLUMN se_supervision_instance.id IS 'PK'; +COMMENT ON COLUMN se_supervision_instance.gmt_create IS 'create time'; +COMMENT ON COLUMN se_supervision_instance.gmt_modified IS 'modification time'; +COMMENT ON COLUMN se_supervision_instance.process_instance_id IS 'process instance id'; +COMMENT ON COLUMN se_supervision_instance.task_instance_id IS 'task instance id'; +COMMENT ON COLUMN se_supervision_instance.supervisor_user_id IS 'supervisor user id'; +COMMENT ON COLUMN se_supervision_instance.supervision_reason IS 'supervision reason'; +COMMENT ON COLUMN se_supervision_instance.supervision_type IS 'supervision type: urge/track/remind'; +COMMENT ON COLUMN se_supervision_instance.status IS 'status: active/closed'; +COMMENT ON COLUMN se_supervision_instance.close_time IS 'close time'; +COMMENT ON COLUMN se_supervision_instance.tenant_id IS 'tenant id'; + +-- =========================================== +-- Notification instance table +-- =========================================== +CREATE TABLE se_notification_instance ( + id NUMBER(19) DEFAULT se_notification_instance_seq.NEXTVAL NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_instance_id NUMBER(19) NOT NULL, + task_instance_id NUMBER(19) DEFAULT NULL, + sender_user_id VARCHAR2(255) NOT NULL, + receiver_user_id VARCHAR2(255) NOT NULL, + notification_type VARCHAR2(64) NOT NULL, + title VARCHAR2(255) DEFAULT NULL, + content VARCHAR2(1000) DEFAULT NULL, + read_status VARCHAR2(64) DEFAULT 'unread' NOT NULL, + read_time TIMESTAMP(6) DEFAULT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + CONSTRAINT pk_notification_instance PRIMARY KEY (id) +); + +COMMENT ON TABLE se_notification_instance IS 'Notification instance table'; +COMMENT ON COLUMN se_notification_instance.id IS 'PK'; +COMMENT ON COLUMN se_notification_instance.gmt_create IS 'create time'; +COMMENT ON COLUMN se_notification_instance.gmt_modified IS 'modification time'; +COMMENT ON COLUMN se_notification_instance.process_instance_id IS 'process instance id'; +COMMENT ON COLUMN se_notification_instance.task_instance_id IS 'task instance id'; +COMMENT ON COLUMN se_notification_instance.sender_user_id IS 'sender user id'; +COMMENT ON COLUMN se_notification_instance.receiver_user_id IS 'receiver user id'; +COMMENT ON COLUMN se_notification_instance.notification_type IS 'notification type: cc/inform'; +COMMENT ON COLUMN se_notification_instance.title IS 'notification title'; +COMMENT ON COLUMN se_notification_instance.content IS 'notification content'; +COMMENT ON COLUMN se_notification_instance.read_status IS 'read status: unread/read'; +COMMENT ON COLUMN se_notification_instance.read_time IS 'read time'; +COMMENT ON COLUMN se_notification_instance.tenant_id IS 'tenant id'; + +-- =========================================== +-- Task transfer record table +-- =========================================== +CREATE TABLE se_task_transfer_record ( + id NUMBER(19) DEFAULT se_task_transfer_record_seq.NEXTVAL NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + task_instance_id NUMBER(19) NOT NULL, + from_user_id VARCHAR2(255) NOT NULL, + to_user_id VARCHAR2(255) NOT NULL, + transfer_reason VARCHAR2(500) DEFAULT NULL, + deadline TIMESTAMP(6) DEFAULT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + CONSTRAINT pk_task_transfer_record PRIMARY KEY (id) +); + +COMMENT ON TABLE se_task_transfer_record IS 'Task transfer record table'; +COMMENT ON COLUMN se_task_transfer_record.id IS 'PK'; +COMMENT ON COLUMN se_task_transfer_record.gmt_create IS 'create time'; +COMMENT ON COLUMN se_task_transfer_record.gmt_modified IS 'modification time'; +COMMENT ON COLUMN se_task_transfer_record.task_instance_id IS 'task instance id'; +COMMENT ON COLUMN se_task_transfer_record.from_user_id IS 'from user id'; +COMMENT ON COLUMN se_task_transfer_record.to_user_id IS 'to user id'; +COMMENT ON COLUMN se_task_transfer_record.transfer_reason IS 'transfer reason'; +COMMENT ON COLUMN se_task_transfer_record.deadline IS 'processing deadline'; +COMMENT ON COLUMN se_task_transfer_record.tenant_id IS 'tenant id'; + +-- =========================================== +-- Assignee operation record table +-- =========================================== +CREATE TABLE se_assignee_operation_record ( + id NUMBER(19) DEFAULT se_assignee_operation_record_seq.NEXTVAL NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + task_instance_id NUMBER(19) NOT NULL, + operation_type VARCHAR2(64) NOT NULL, + operator_user_id VARCHAR2(255) NOT NULL, + target_user_id VARCHAR2(255) NOT NULL, + operation_reason VARCHAR2(500) DEFAULT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + CONSTRAINT pk_assignee_operation_record PRIMARY KEY (id) +); + +COMMENT ON TABLE se_assignee_operation_record IS 'Assignee operation record table'; +COMMENT ON COLUMN se_assignee_operation_record.id IS 'PK'; +COMMENT ON COLUMN se_assignee_operation_record.gmt_create IS 'create time'; +COMMENT ON COLUMN se_assignee_operation_record.gmt_modified IS 'modification time'; +COMMENT ON COLUMN se_assignee_operation_record.task_instance_id IS 'task instance id'; +COMMENT ON COLUMN se_assignee_operation_record.operation_type IS 'operation type: add_assignee/remove_assignee'; +COMMENT ON COLUMN se_assignee_operation_record.operator_user_id IS 'operator user id'; +COMMENT ON COLUMN se_assignee_operation_record.target_user_id IS 'target user id'; +COMMENT ON COLUMN se_assignee_operation_record.operation_reason IS 'operation reason'; +COMMENT ON COLUMN se_assignee_operation_record.tenant_id IS 'tenant id'; + +-- =========================================== +-- Process rollback record table +-- =========================================== +CREATE TABLE se_process_rollback_record ( + id NUMBER(19) DEFAULT se_process_rollback_record_seq.NEXTVAL NOT NULL, + gmt_create TIMESTAMP(6) NOT NULL, + gmt_modified TIMESTAMP(6) NOT NULL, + process_instance_id NUMBER(19) NOT NULL, + task_instance_id NUMBER(19) NOT NULL, + rollback_type VARCHAR2(64) NOT NULL, + from_activity_id VARCHAR2(255) NOT NULL, + to_activity_id VARCHAR2(255) NOT NULL, + operator_user_id VARCHAR2(255) NOT NULL, + rollback_reason VARCHAR2(500) DEFAULT NULL, + tenant_id VARCHAR2(64) DEFAULT NULL, + CONSTRAINT pk_process_rollback_record PRIMARY KEY (id) +); + +COMMENT ON TABLE se_process_rollback_record IS 'Process rollback record table'; +COMMENT ON COLUMN se_process_rollback_record.id IS 'PK'; +COMMENT ON COLUMN se_process_rollback_record.gmt_create IS 'create time'; +COMMENT ON COLUMN se_process_rollback_record.gmt_modified IS 'modification time'; +COMMENT ON COLUMN se_process_rollback_record.process_instance_id IS 'process instance id'; +COMMENT ON COLUMN se_process_rollback_record.task_instance_id IS 'task instance id'; +COMMENT ON COLUMN se_process_rollback_record.rollback_type IS 'rollback type: previous/specific'; +COMMENT ON COLUMN se_process_rollback_record.from_activity_id IS 'from activity id'; +COMMENT ON COLUMN se_process_rollback_record.to_activity_id IS 'to activity id'; +COMMENT ON COLUMN se_process_rollback_record.operator_user_id IS 'operator user id'; +COMMENT ON COLUMN se_process_rollback_record.rollback_reason IS 'rollback reason'; +COMMENT ON COLUMN se_process_rollback_record.tenant_id IS 'tenant id'; diff --git a/extension/storage/storage-mysql/src/main/resources/sql/workflow-enhancement-schema-postgresql.sql b/extension/storage/storage-mysql/src/main/resources/sql/workflow-enhancement-schema-postgresql.sql new file mode 100644 index 000000000..707d2480a --- /dev/null +++ b/extension/storage/storage-mysql/src/main/resources/sql/workflow-enhancement-schema-postgresql.sql @@ -0,0 +1,169 @@ +-- Workflow enhancement database schema for PostgreSQL +-- Based on SmartEngine existing table structure design + +-- =========================================== +-- Supervision instance table +-- =========================================== +CREATE TABLE se_supervision_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint NOT NULL, + supervisor_user_id varchar(255) NOT NULL, + supervision_reason varchar(500), + supervision_type varchar(64) NOT NULL, + status varchar(64) NOT NULL, + close_time timestamp(6), + tenant_id varchar(64) +); + +-- Composite index for findActiveByTaskId (high frequency query) +CREATE INDEX idx_supervision_task_status_tenant + ON se_supervision_instance (task_instance_id, status, tenant_id); + +-- Composite index for findBySupervisor +CREATE INDEX idx_supervision_supervisor_tenant + ON se_supervision_instance (supervisor_user_id, tenant_id); + +-- Single column index for cross-process queries +CREATE INDEX idx_supervision_process_instance_id + ON se_supervision_instance (process_instance_id); + +COMMENT ON TABLE se_supervision_instance IS 'Supervision instance table'; +COMMENT ON COLUMN se_supervision_instance.supervision_type IS 'urge/track/remind'; +COMMENT ON COLUMN se_supervision_instance.status IS 'active/closed'; + +-- =========================================== +-- Notification instance table +-- =========================================== +CREATE TABLE se_notification_instance ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint, + sender_user_id varchar(255) NOT NULL, + receiver_user_id varchar(255) NOT NULL, + notification_type varchar(64) NOT NULL, + title varchar(255), + content varchar(1000), + read_status varchar(64) NOT NULL DEFAULT 'unread', + read_time timestamp(6), + tenant_id varchar(64) +); + +-- Composite index for findByReceiver (highest frequency query - user unread messages) +CREATE INDEX idx_notification_receiver_read_tenant + ON se_notification_instance (receiver_user_id, read_status, tenant_id); + +-- Composite index for findBySender +CREATE INDEX idx_notification_sender_tenant + ON se_notification_instance (sender_user_id, tenant_id); + +-- Single column indexes for cross-entity queries +CREATE INDEX idx_notification_process_instance_id + ON se_notification_instance (process_instance_id); + +CREATE INDEX idx_notification_task_instance_id + ON se_notification_instance (task_instance_id); + +COMMENT ON TABLE se_notification_instance IS 'Notification instance table'; +COMMENT ON COLUMN se_notification_instance.notification_type IS 'cc/inform'; +COMMENT ON COLUMN se_notification_instance.read_status IS 'unread/read'; + +-- =========================================== +-- Task transfer record table +-- =========================================== +CREATE TABLE se_task_transfer_record ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + task_instance_id bigint NOT NULL, + process_instance_id bigint, + from_user_id varchar(255) NOT NULL, + to_user_id varchar(255) NOT NULL, + transfer_reason varchar(500), + deadline timestamp(6), + tenant_id varchar(64) +); + +-- Composite index for selectByTaskInstanceId +CREATE INDEX idx_task_transfer_task_tenant + ON se_task_transfer_record (task_instance_id, tenant_id); + +-- Single column indexes for user queries +CREATE INDEX idx_task_transfer_from_user_id + ON se_task_transfer_record (from_user_id); + +CREATE INDEX idx_task_transfer_to_user_id + ON se_task_transfer_record (to_user_id); + +COMMENT ON TABLE se_task_transfer_record IS 'Task transfer record table'; + +-- =========================================== +-- Assignee operation record table +-- =========================================== +CREATE TABLE se_assignee_operation_record ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + task_instance_id bigint NOT NULL, + process_instance_id bigint, + operation_type varchar(64) NOT NULL, + operator_user_id varchar(255) NOT NULL, + target_user_id varchar(255) NOT NULL, + operation_reason varchar(500), + tenant_id varchar(64) +); + +-- Composite index for selectByTaskInstanceId +CREATE INDEX idx_assignee_op_task_tenant + ON se_assignee_operation_record (task_instance_id, tenant_id); + +-- Single column indexes for audit queries +CREATE INDEX idx_assignee_op_operation_type + ON se_assignee_operation_record (operation_type); + +CREATE INDEX idx_assignee_op_operator_user_id + ON se_assignee_operation_record (operator_user_id); + +CREATE INDEX idx_assignee_op_target_user_id + ON se_assignee_operation_record (target_user_id); + +COMMENT ON TABLE se_assignee_operation_record IS 'Assignee operation record table'; +COMMENT ON COLUMN se_assignee_operation_record.operation_type IS 'add_assignee/remove_assignee'; + +-- =========================================== +-- Process rollback record table +-- =========================================== +CREATE TABLE se_process_rollback_record ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + gmt_create timestamp(6) NOT NULL, + gmt_modified timestamp(6) NOT NULL, + process_instance_id bigint NOT NULL, + task_instance_id bigint NOT NULL, + rollback_type varchar(64) NOT NULL, + from_activity_id varchar(255) NOT NULL, + to_activity_id varchar(255) NOT NULL, + operator_user_id varchar(255) NOT NULL, + rollback_reason varchar(500), + tenant_id varchar(64) +); + +-- Composite index for selectByProcessInstanceId +CREATE INDEX idx_rollback_process_tenant + ON se_process_rollback_record (process_instance_id, tenant_id); + +-- Single column indexes for audit queries +CREATE INDEX idx_rollback_task_instance_id + ON se_process_rollback_record (task_instance_id); + +CREATE INDEX idx_rollback_type + ON se_process_rollback_record (rollback_type); + +CREATE INDEX idx_rollback_operator_user_id + ON se_process_rollback_record (operator_user_id); + +COMMENT ON TABLE se_process_rollback_record IS 'Process rollback record table'; +COMMENT ON COLUMN se_process_rollback_record.rollback_type IS 'previous/specific'; diff --git a/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/persister/database/dao/NotificationInstanceDAOTest.java b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/persister/database/dao/NotificationInstanceDAOTest.java new file mode 100644 index 000000000..dab67cb84 --- /dev/null +++ b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/persister/database/dao/NotificationInstanceDAOTest.java @@ -0,0 +1,172 @@ +package com.alibaba.smart.framework.engine.persister.database.dao; + +import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.constant.NotificationConstant; +import com.alibaba.smart.framework.engine.persister.database.entity.NotificationInstanceEntity; +import lombok.Setter; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Arrays; +import java.util.List; + +/** + * 知会通知实例DAO测试 + * + * @author SmartEngine Team + */ +public class NotificationInstanceDAOTest extends BaseElementTest { + + @Setter(onMethod = @__({@Autowired})) + NotificationInstanceDAO notificationInstanceDAO; + + NotificationInstanceEntity notificationInstance = null; + String tenantId = "test-tenant"; + + @Before + public void before() { + notificationInstance = new NotificationInstanceEntity(); + notificationInstance.setId(1L); + notificationInstance.setGmtCreate(DateUtil.getCurrentDate()); + notificationInstance.setGmtModified(DateUtil.getCurrentDate()); + notificationInstance.setProcessInstanceId(100L); + notificationInstance.setTaskInstanceId(200L); + notificationInstance.setSenderUserId("sender001"); + notificationInstance.setReceiverUserId("receiver001"); + notificationInstance.setNotificationType(NotificationConstant.NotificationType.CC); + notificationInstance.setTitle("测试通知标题"); + notificationInstance.setContent("测试通知内容"); + notificationInstance.setReadStatus(NotificationConstant.ReadStatus.UNREAD); + notificationInstance.setTenantId(tenantId); + } + + @Test + public void testInsert() { + notificationInstanceDAO.insert(notificationInstance); + Assert.assertNotNull(notificationInstance.getId()); + } + + @Test + public void testFindOne() { + notificationInstanceDAO.insert(notificationInstance); + + NotificationInstanceEntity found = notificationInstanceDAO.findOne(notificationInstance.getId(), tenantId); + Assert.assertNotNull(found); + Assert.assertEquals(notificationInstance.getSenderUserId(), found.getSenderUserId()); + Assert.assertEquals(notificationInstance.getReceiverUserId(), found.getReceiverUserId()); + Assert.assertEquals(notificationInstance.getTitle(), found.getTitle()); + Assert.assertEquals(notificationInstance.getReadStatus(), found.getReadStatus()); + } + + @Test + public void testFindByReceiver() { + notificationInstanceDAO.insert(notificationInstance); + + List notificationList = notificationInstanceDAO + .findByReceiver("receiver001", NotificationConstant.ReadStatus.UNREAD, tenantId, 0, 10); + + Assert.assertNotNull(notificationList); + Assert.assertTrue(notificationList.size() > 0); + Assert.assertEquals("receiver001", notificationList.get(0).getReceiverUserId()); + Assert.assertEquals(NotificationConstant.ReadStatus.UNREAD, notificationList.get(0).getReadStatus()); + } + + @Test + public void testFindBySender() { + notificationInstanceDAO.insert(notificationInstance); + + List notificationList = notificationInstanceDAO + .findBySender("sender001", tenantId, 0, 10); + + Assert.assertNotNull(notificationList); + Assert.assertTrue(notificationList.size() > 0); + Assert.assertEquals("sender001", notificationList.get(0).getSenderUserId()); + } + + @Test + public void testCountByReceiver() { + notificationInstanceDAO.insert(notificationInstance); + + Integer count = notificationInstanceDAO.countByReceiver("receiver001", + NotificationConstant.ReadStatus.UNREAD, tenantId); + Assert.assertNotNull(count); + Assert.assertTrue(count > 0); + } + + @Test + public void testCountBySender() { + notificationInstanceDAO.insert(notificationInstance); + + Integer count = notificationInstanceDAO.countBySender("sender001", tenantId); + Assert.assertNotNull(count); + Assert.assertTrue(count > 0); + } + + @Test + public void testMarkAsRead() { + notificationInstanceDAO.insert(notificationInstance); + + int result = notificationInstanceDAO.markAsRead(notificationInstance.getId(), tenantId); + Assert.assertEquals(1, result); + + NotificationInstanceEntity found = notificationInstanceDAO.findOne(notificationInstance.getId(), tenantId); + Assert.assertEquals(NotificationConstant.ReadStatus.READ, found.getReadStatus()); + Assert.assertNotNull(found.getReadTime()); + } + + @Test + public void testBatchMarkAsRead() { + // 插入多个通知 + notificationInstanceDAO.insert(notificationInstance); + + NotificationInstanceEntity notification2 = new NotificationInstanceEntity(); + notification2.setId(2L); + notification2.setGmtCreate(DateUtil.getCurrentDate()); + notification2.setGmtModified(DateUtil.getCurrentDate()); + notification2.setProcessInstanceId(101L); + notification2.setTaskInstanceId(201L); + notification2.setSenderUserId("sender001"); + notification2.setReceiverUserId("receiver001"); + notification2.setNotificationType(NotificationConstant.NotificationType.INFORM); + notification2.setTitle("测试通知标题2"); + notification2.setContent("测试通知内容2"); + notification2.setReadStatus(NotificationConstant.ReadStatus.UNREAD); + notification2.setTenantId(tenantId); + notificationInstanceDAO.insert(notification2); + + List ids = Arrays.asList(notificationInstance.getId(), notification2.getId()); + int result = notificationInstanceDAO.batchMarkAsRead(ids, tenantId); + Assert.assertEquals(2, result); + + NotificationInstanceEntity found1 = notificationInstanceDAO.findOne(notificationInstance.getId(), tenantId); + NotificationInstanceEntity found2 = notificationInstanceDAO.findOne(notification2.getId(), tenantId); + Assert.assertEquals(NotificationConstant.ReadStatus.READ, found1.getReadStatus()); + Assert.assertEquals(NotificationConstant.ReadStatus.READ, found2.getReadStatus()); + } + + @Test + public void testUpdate() { + notificationInstanceDAO.insert(notificationInstance); + + notificationInstance.setTitle("更新后的标题"); + notificationInstance.setContent("更新后的内容"); + notificationInstanceDAO.update(notificationInstance); + + NotificationInstanceEntity found = notificationInstanceDAO.findOne(notificationInstance.getId(), tenantId); + Assert.assertEquals("更新后的标题", found.getTitle()); + Assert.assertEquals("更新后的内容", found.getContent()); + } + + @Test + public void testDelete() { + notificationInstanceDAO.insert(notificationInstance); + Long id = notificationInstance.getId(); + + notificationInstanceDAO.delete(id, tenantId); + + NotificationInstanceEntity found = notificationInstanceDAO.findOne(id, tenantId); + Assert.assertNull(found); + } +} \ No newline at end of file diff --git a/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/persister/database/dao/ShardingIndexMaintenanceTest.java b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/persister/database/dao/ShardingIndexMaintenanceTest.java new file mode 100644 index 000000000..71422eb76 --- /dev/null +++ b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/persister/database/dao/ShardingIndexMaintenanceTest.java @@ -0,0 +1,340 @@ +package com.alibaba.smart.framework.engine.persister.database.dao; + +import java.util.List; + +import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.constant.TaskInstanceConstant; +import com.alibaba.smart.framework.engine.constant.NotificationConstant; +import com.alibaba.smart.framework.engine.persister.database.entity.NotificationInstanceEntity; +import com.alibaba.smart.framework.engine.persister.database.entity.TaskAssigneeEntity; +import com.alibaba.smart.framework.engine.persister.database.entity.TaskInstanceEntity; +import com.alibaba.smart.framework.engine.persister.database.entity.UserNotificationIndexEntity; +import com.alibaba.smart.framework.engine.persister.database.entity.UserTaskIndexEntity; +import com.alibaba.smart.framework.engine.service.param.query.TaskInstanceQueryByAssigneeParam; + +import lombok.Setter; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Integration tests verifying sharding index table maintenance workflows. + * Simulates the Storage layer's index maintenance logic at DAO level. + */ +public class ShardingIndexMaintenanceTest extends BaseElementTest { + + @Setter(onMethod = @__({@Autowired})) + TaskInstanceDAO taskDao; + + @Setter(onMethod = @__({@Autowired})) + TaskAssigneeDAO taskAssigneeDAO; + + @Setter(onMethod = @__({@Autowired})) + UserTaskIndexDAO userTaskIndexDAO; + + @Setter(onMethod = @__({@Autowired})) + NotificationInstanceDAO notificationInstanceDAO; + + @Setter(onMethod = @__({@Autowired})) + UserNotificationIndexDAO userNotificationIndexDAO; + + String tenantId = "sharding-integ-test"; + + TaskInstanceEntity task; + TaskAssigneeEntity assignee1; + TaskAssigneeEntity assignee2; + + @Before + public void before() { + // Task instance + task = new TaskInstanceEntity(); + task.setId(8001L); + task.setGmtCreate(DateUtil.getCurrentDate()); + task.setGmtModified(DateUtil.getCurrentDate()); + task.setProcessInstanceId(9001L); + task.setProcessDefinitionIdAndVersion("approval:1.0"); + task.setProcessDefinitionType("approval"); + task.setActivityInstanceId(7001L); + task.setProcessDefinitionActivityId("userTask1"); + task.setExecutionInstanceId(6001L); + task.setStatus(TaskInstanceConstant.PENDING); + task.setTitle("Approval Request"); + task.setPriority(500); + task.setDomainCode("HR"); + task.setTenantId(tenantId); + + // Assignee 1 + assignee1 = new TaskAssigneeEntity(); + assignee1.setId(8101L); + assignee1.setGmtCreate(DateUtil.getCurrentDate()); + assignee1.setGmtModified(DateUtil.getCurrentDate()); + assignee1.setProcessInstanceId(9001L); + assignee1.setTaskInstanceId(8001L); + assignee1.setAssigneeId("approver001"); + assignee1.setAssigneeType("user"); + assignee1.setTenantId(tenantId); + + // Assignee 2 + assignee2 = new TaskAssigneeEntity(); + assignee2.setId(8102L); + assignee2.setGmtCreate(DateUtil.getCurrentDate()); + assignee2.setGmtModified(DateUtil.getCurrentDate()); + assignee2.setProcessInstanceId(9001L); + assignee2.setTaskInstanceId(8001L); + assignee2.setAssigneeId("approver002"); + assignee2.setAssigneeType("user"); + assignee2.setTenantId(tenantId); + } + + // ============================================= + // Test: Task assignee insert → index table populated + // ============================================= + + @Test + public void testAssigneeInsert_shouldPopulateIndex() { + // Step 1: Insert task + taskDao.insert(task); + + // Step 2: Insert assignee (simulating what Storage layer does) + taskAssigneeDAO.insert(assignee1); + + // Step 3: Build index entry (simulating Storage layer's sharding logic) + UserTaskIndexEntity indexEntity = buildUserTaskIndex(task, assignee1); + userTaskIndexDAO.insert(indexEntity); + + // Verify: Index table should have the entry + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(tenantId); + param.setAssigneeUserId("approver001"); + + List results = userTaskIndexDAO.findByAssignee(param); + Assert.assertEquals(1, results.size()); + Assert.assertEquals("Approval Request", results.get(0).getTitle()); + Assert.assertEquals("approval", results.get(0).getProcessDefinitionType()); + Assert.assertEquals("HR", results.get(0).getDomainCode()); + Assert.assertEquals(Long.valueOf(9001L), results.get(0).getProcessInstanceId()); + } + + @Test + public void testMultipleAssignees_shouldCreateMultipleIndexEntries() { + taskDao.insert(task); + taskAssigneeDAO.insert(assignee1); + taskAssigneeDAO.insert(assignee2); + + userTaskIndexDAO.insert(buildUserTaskIndex(task, assignee1)); + userTaskIndexDAO.insert(buildUserTaskIndex(task, assignee2)); + + // Both assignees should see the task + TaskInstanceQueryByAssigneeParam param1 = new TaskInstanceQueryByAssigneeParam(); + param1.setTenantId(tenantId); + param1.setAssigneeUserId("approver001"); + Assert.assertEquals(Integer.valueOf(1), userTaskIndexDAO.countByAssignee(param1)); + + TaskInstanceQueryByAssigneeParam param2 = new TaskInstanceQueryByAssigneeParam(); + param2.setTenantId(tenantId); + param2.setAssigneeUserId("approver002"); + Assert.assertEquals(Integer.valueOf(1), userTaskIndexDAO.countByAssignee(param2)); + } + + // ============================================= + // Test: Task complete → index entries removed + // ============================================= + + @Test + public void testTaskComplete_shouldRemoveIndexEntries() { + // Setup: task + assignees + index entries + taskDao.insert(task); + taskAssigneeDAO.insert(assignee1); + taskAssigneeDAO.insert(assignee2); + userTaskIndexDAO.insert(buildUserTaskIndex(task, assignee1)); + userTaskIndexDAO.insert(buildUserTaskIndex(task, assignee2)); + + // Verify index entries exist + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(tenantId); + param.setAssigneeUserId("approver001"); + Assert.assertEquals(Integer.valueOf(1), userTaskIndexDAO.countByAssignee(param)); + + // Complete the task (update status) + task.setStatus(TaskInstanceConstant.COMPLETED); + task.setGmtModified(DateUtil.getCurrentDate()); + taskDao.update(task); + + // Simulating Storage layer: delete index entries on terminal status + userTaskIndexDAO.deleteByTaskInstanceId(8001L, tenantId); + + // Verify: All index entries for this task should be gone + param.setAssigneeUserId("approver001"); + Assert.assertEquals(Integer.valueOf(0), userTaskIndexDAO.countByAssignee(param)); + + param.setAssigneeUserId("approver002"); + Assert.assertEquals(Integer.valueOf(0), userTaskIndexDAO.countByAssignee(param)); + } + + // ============================================= + // Test: Assignee remove → only that index entry removed + // ============================================= + + @Test + public void testAssigneeRemove_shouldRemoveOnlyThatIndexEntry() { + taskDao.insert(task); + taskAssigneeDAO.insert(assignee1); + taskAssigneeDAO.insert(assignee2); + userTaskIndexDAO.insert(buildUserTaskIndex(task, assignee1)); + userTaskIndexDAO.insert(buildUserTaskIndex(task, assignee2)); + + // Remove assignee1 + taskAssigneeDAO.delete(assignee1.getId(), tenantId); + userTaskIndexDAO.deleteByAssigneeAndTask("approver001", 8001L, tenantId); + + // approver001 should have no tasks + TaskInstanceQueryByAssigneeParam param1 = new TaskInstanceQueryByAssigneeParam(); + param1.setTenantId(tenantId); + param1.setAssigneeUserId("approver001"); + Assert.assertEquals(Integer.valueOf(0), userTaskIndexDAO.countByAssignee(param1)); + + // approver002 should still have the task + TaskInstanceQueryByAssigneeParam param2 = new TaskInstanceQueryByAssigneeParam(); + param2.setTenantId(tenantId); + param2.setAssigneeUserId("approver002"); + Assert.assertEquals(Integer.valueOf(1), userTaskIndexDAO.countByAssignee(param2)); + } + + // ============================================= + // Test: Notification index maintenance + // ============================================= + + @Test + public void testNotificationInsert_shouldPopulateIndex() { + NotificationInstanceEntity notification = new NotificationInstanceEntity(); + notification.setId(9501L); + notification.setGmtCreate(DateUtil.getCurrentDate()); + notification.setGmtModified(DateUtil.getCurrentDate()); + notification.setProcessInstanceId(9001L); + notification.setTaskInstanceId(8001L); + notification.setSenderUserId("sender001"); + notification.setReceiverUserId("receiver001"); + notification.setNotificationType(NotificationConstant.NotificationType.CC); + notification.setTitle("CC Notification"); + notification.setReadStatus(NotificationConstant.ReadStatus.UNREAD); + notification.setTenantId(tenantId); + + notificationInstanceDAO.insert(notification); + + // Build index entry (simulating Storage layer) + UserNotificationIndexEntity indexEntity = new UserNotificationIndexEntity(); + indexEntity.setTenantId(tenantId); + indexEntity.setReceiverUserId("receiver001"); + indexEntity.setNotificationId(9501L); + indexEntity.setProcessInstanceId(9001L); + indexEntity.setNotificationType(NotificationConstant.NotificationType.CC); + indexEntity.setTitle("CC Notification"); + indexEntity.setReadStatus(NotificationConstant.ReadStatus.UNREAD); + indexEntity.setGmtCreate(DateUtil.getCurrentDate()); + indexEntity.setGmtModified(DateUtil.getCurrentDate()); + userNotificationIndexDAO.insert(indexEntity); + + // Verify + Integer count = userNotificationIndexDAO.countByReceiver( + "receiver001", NotificationConstant.ReadStatus.UNREAD, tenantId); + Assert.assertEquals(Integer.valueOf(1), count); + } + + @Test + public void testNotificationMarkAsRead_shouldUpdateIndex() { + NotificationInstanceEntity notification = new NotificationInstanceEntity(); + notification.setId(9502L); + notification.setGmtCreate(DateUtil.getCurrentDate()); + notification.setGmtModified(DateUtil.getCurrentDate()); + notification.setProcessInstanceId(9001L); + notification.setSenderUserId("sender001"); + notification.setReceiverUserId("receiver001"); + notification.setNotificationType(NotificationConstant.NotificationType.INFORM); + notification.setTitle("Inform Notification"); + notification.setReadStatus(NotificationConstant.ReadStatus.UNREAD); + notification.setTenantId(tenantId); + + notificationInstanceDAO.insert(notification); + + UserNotificationIndexEntity indexEntity = new UserNotificationIndexEntity(); + indexEntity.setTenantId(tenantId); + indexEntity.setReceiverUserId("receiver001"); + indexEntity.setNotificationId(9502L); + indexEntity.setProcessInstanceId(9001L); + indexEntity.setNotificationType(NotificationConstant.NotificationType.INFORM); + indexEntity.setTitle("Inform Notification"); + indexEntity.setReadStatus(NotificationConstant.ReadStatus.UNREAD); + indexEntity.setGmtCreate(DateUtil.getCurrentDate()); + indexEntity.setGmtModified(DateUtil.getCurrentDate()); + userNotificationIndexDAO.insert(indexEntity); + + // Mark as read in main table + notificationInstanceDAO.markAsRead(9502L, tenantId); + + // Sync to index table + userNotificationIndexDAO.updateReadStatus( + 9502L, NotificationConstant.ReadStatus.READ, tenantId); + + // Verify: unread count should be 0 + Integer unreadCount = userNotificationIndexDAO.countByReceiver( + "receiver001", NotificationConstant.ReadStatus.UNREAD, tenantId); + Assert.assertEquals(Integer.valueOf(0), unreadCount); + } + + // ============================================= + // Test: Query from index table returns correct data + // ============================================= + + @Test + public void testQueryFromIndex_matchesMainTableData() { + taskDao.insert(task); + taskAssigneeDAO.insert(assignee1); + userTaskIndexDAO.insert(buildUserTaskIndex(task, assignee1)); + + // Query from index + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(tenantId); + param.setAssigneeUserId("approver001"); + param.setStatus(TaskInstanceConstant.PENDING); + param.setProcessDefinitionType("approval"); + + List indexResults = userTaskIndexDAO.findByAssignee(param); + Assert.assertEquals(1, indexResults.size()); + + UserTaskIndexEntity indexRow = indexResults.get(0); + + // Query from main table + TaskInstanceEntity mainRow = taskDao.findOne(8001L, tenantId); + + // Verify data consistency + Assert.assertEquals(mainRow.getProcessInstanceId(), indexRow.getProcessInstanceId()); + Assert.assertEquals(mainRow.getTitle(), indexRow.getTitle()); + Assert.assertEquals(mainRow.getPriority(), indexRow.getPriority()); + Assert.assertEquals(mainRow.getProcessDefinitionType(), indexRow.getProcessDefinitionType()); + Assert.assertEquals(mainRow.getDomainCode(), indexRow.getDomainCode()); + } + + // ============================================= + // Helper methods + // ============================================= + + private UserTaskIndexEntity buildUserTaskIndex(TaskInstanceEntity task, TaskAssigneeEntity assignee) { + UserTaskIndexEntity entity = new UserTaskIndexEntity(); + entity.setTenantId(task.getTenantId()); + entity.setAssigneeId(assignee.getAssigneeId()); + entity.setAssigneeType(assignee.getAssigneeType()); + entity.setTaskInstanceId(task.getId()); + entity.setProcessInstanceId(task.getProcessInstanceId()); + entity.setProcessDefinitionType(task.getProcessDefinitionType()); + entity.setDomainCode(task.getDomainCode()); + entity.setExtra(task.getExtra()); + entity.setTaskStatus(task.getStatus()); + entity.setTaskGmtModified(task.getGmtModified()); + entity.setTitle(task.getTitle()); + entity.setPriority(task.getPriority()); + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + return entity; + } +} diff --git a/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/persister/database/dao/SupervisionInstanceDAOTest.java b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/persister/database/dao/SupervisionInstanceDAOTest.java new file mode 100644 index 000000000..63efa74d6 --- /dev/null +++ b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/persister/database/dao/SupervisionInstanceDAOTest.java @@ -0,0 +1,135 @@ +package com.alibaba.smart.framework.engine.persister.database.dao; + +import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.constant.SupervisionConstant; +import com.alibaba.smart.framework.engine.persister.database.entity.SupervisionInstanceEntity; +import lombok.Setter; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +/** + * 督办实例DAO测试 + * + * @author SmartEngine Team + */ +public class SupervisionInstanceDAOTest extends BaseElementTest { + + @Setter(onMethod = @__({@Autowired})) + SupervisionInstanceDAO supervisionInstanceDAO; + + SupervisionInstanceEntity supervisionInstance = null; + String tenantId = "test-tenant"; + + @Before + public void before() { + supervisionInstance = new SupervisionInstanceEntity(); + supervisionInstance.setId(1L); + supervisionInstance.setGmtCreate(DateUtil.getCurrentDate()); + supervisionInstance.setGmtModified(DateUtil.getCurrentDate()); + supervisionInstance.setProcessInstanceId(100L); + supervisionInstance.setTaskInstanceId(200L); + supervisionInstance.setSupervisorUserId("supervisor001"); + supervisionInstance.setSupervisionReason("测试督办原因"); + supervisionInstance.setSupervisionType(SupervisionConstant.SupervisionType.URGE); + supervisionInstance.setStatus(SupervisionConstant.SupervisionStatus.ACTIVE); + supervisionInstance.setTenantId(tenantId); + } + + @Test + public void testInsert() { + supervisionInstanceDAO.insert(supervisionInstance); + Assert.assertNotNull(supervisionInstance.getId()); + } + + @Test + public void testFindOne() { + supervisionInstanceDAO.insert(supervisionInstance); + + SupervisionInstanceEntity found = supervisionInstanceDAO.findOne(supervisionInstance.getId(), tenantId); + Assert.assertNotNull(found); + Assert.assertEquals(supervisionInstance.getSupervisorUserId(), found.getSupervisorUserId()); + Assert.assertEquals(supervisionInstance.getSupervisionType(), found.getSupervisionType()); + Assert.assertEquals(supervisionInstance.getStatus(), found.getStatus()); + } + + @Test + public void testFindBySupervisor() { + supervisionInstanceDAO.insert(supervisionInstance); + + List supervisionList = supervisionInstanceDAO + .findBySupervisor("supervisor001", tenantId, 0, 10); + + Assert.assertNotNull(supervisionList); + Assert.assertTrue(supervisionList.size() > 0); + Assert.assertEquals("supervisor001", supervisionList.get(0).getSupervisorUserId()); + } + + @Test + public void testFindActiveByTaskId() { + supervisionInstanceDAO.insert(supervisionInstance); + + List supervisionList = supervisionInstanceDAO + .findActiveByTaskId(200L, tenantId); + + Assert.assertNotNull(supervisionList); + Assert.assertTrue(supervisionList.size() > 0); + Assert.assertEquals(SupervisionConstant.SupervisionStatus.ACTIVE, supervisionList.get(0).getStatus()); + } + + @Test + public void testCountBySupervisor() { + supervisionInstanceDAO.insert(supervisionInstance); + + Integer count = supervisionInstanceDAO.countBySupervisor("supervisor001", tenantId); + Assert.assertNotNull(count); + Assert.assertTrue(count > 0); + } + + @Test + public void testCloseSupervision() { + supervisionInstanceDAO.insert(supervisionInstance); + + int result = supervisionInstanceDAO.closeSupervision(supervisionInstance.getId(), tenantId); + Assert.assertEquals(1, result); + + SupervisionInstanceEntity found = supervisionInstanceDAO.findOne(supervisionInstance.getId(), tenantId); + Assert.assertEquals(SupervisionConstant.SupervisionStatus.CLOSED, found.getStatus()); + } + + @Test + public void testCloseByTaskId() { + supervisionInstanceDAO.insert(supervisionInstance); + + int result = supervisionInstanceDAO.closeByTaskId(200L, tenantId); + Assert.assertEquals(1, result); + + SupervisionInstanceEntity found = supervisionInstanceDAO.findOne(supervisionInstance.getId(), tenantId); + Assert.assertEquals(SupervisionConstant.SupervisionStatus.CLOSED, found.getStatus()); + } + + @Test + public void testUpdate() { + supervisionInstanceDAO.insert(supervisionInstance); + + supervisionInstance.setSupervisionReason("更新后的督办原因"); + supervisionInstanceDAO.update(supervisionInstance); + + SupervisionInstanceEntity found = supervisionInstanceDAO.findOne(supervisionInstance.getId(), tenantId); + Assert.assertEquals("更新后的督办原因", found.getSupervisionReason()); + } + + @Test + public void testDelete() { + supervisionInstanceDAO.insert(supervisionInstance); + Long id = supervisionInstance.getId(); + + supervisionInstanceDAO.delete(id, tenantId); + + SupervisionInstanceEntity found = supervisionInstanceDAO.findOne(id, tenantId); + Assert.assertNull(found); + } +} \ No newline at end of file diff --git a/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/persister/database/dao/TaskInstanceDomainCodeAndExtraTest.java b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/persister/database/dao/TaskInstanceDomainCodeAndExtraTest.java new file mode 100644 index 000000000..7fac1c803 --- /dev/null +++ b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/persister/database/dao/TaskInstanceDomainCodeAndExtraTest.java @@ -0,0 +1,239 @@ +package com.alibaba.smart.framework.engine.persister.database.dao; + +import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.constant.TaskInstanceConstant; +import com.alibaba.smart.framework.engine.dialect.impl.PostgreSqlDialect; +import com.alibaba.smart.framework.engine.persister.database.entity.TaskAssigneeEntity; +import com.alibaba.smart.framework.engine.persister.database.entity.TaskInstanceEntity; +import com.alibaba.smart.framework.engine.service.param.query.JsonCondition; +import com.alibaba.smart.framework.engine.service.param.query.JsonInCondition; +import com.alibaba.smart.framework.engine.service.param.query.TaskInstanceQueryByAssigneeParam; +import com.alibaba.smart.framework.engine.service.param.query.TaskInstanceQueryParam; +import lombok.Setter; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tests for domain_code and extra JSON fields on se_task_instance. + */ +public class TaskInstanceDomainCodeAndExtraTest extends BaseElementTest { + + @Setter(onMethod = @__({@Autowired})) + TaskInstanceDAO taskDao; + + @Setter(onMethod = @__({@Autowired})) + TaskAssigneeDAO taskAssigneeDAO; + + private final String tenantId = "-5"; + private final PostgreSqlDialect dialect = new PostgreSqlDialect(); + + private static List listOf(T... items) { + List list = new ArrayList(); + for (T item : items) { + list.add(item); + } + return list; + } + + private TaskInstanceEntity createTask(Long id, String domainCode, String extra) { + TaskInstanceEntity task = new TaskInstanceEntity(); + task.setId(id); + task.setGmtCreate(DateUtil.getCurrentDate()); + task.setGmtModified(DateUtil.getCurrentDate()); + task.setProcessDefinitionIdAndVersion("test:1.0"); + task.setActivityInstanceId(100L); + task.setClaimUserId("user1"); + task.setExecutionInstanceId(200L); + task.setPriority(500); + task.setStatus(TaskInstanceConstant.PENDING); + task.setProcessDefinitionActivityId("userTask1"); + task.setProcessInstanceId(300L); + task.setProcessDefinitionType("approval"); + task.setTag("tag1"); + task.setTenantId(tenantId); + task.setDomainCode(domainCode); + task.setExtra(extra); + return task; + } + + @Before + public void before() { + TaskInstanceEntity t1 = createTask(5001L, "HR", "{\"category\":\"leave\",\"priority_level\":\"high\",\"department\":\"研发一部\"}"); + TaskInstanceEntity t2 = createTask(5002L, "Finance", "{\"category\":\"purchase\",\"priority_level\":\"low\",\"department\":\"财务部\"}"); + TaskInstanceEntity t3 = createTask(5003L, "HR", "{\"category\":\"purchase\",\"priority_level\":\"critical\",\"department\":\"研发二部\"}"); + TaskInstanceEntity t4 = createTask(5004L, null, null); + + taskDao.insert(t1); + taskDao.insert(t2); + taskDao.insert(t3); + taskDao.insert(t4); + } + + // ============ domain_code tests ============ + + @Test + public void testDomainCodeExactMatch() { + TaskInstanceQueryParam param = new TaskInstanceQueryParam(); + param.setTenantId(tenantId); + param.setDomainCode("HR"); + + List result = taskDao.findTaskList(param); + Assert.assertEquals(2, result.size()); + } + + @Test + public void testDomainCodeIn() { + TaskInstanceQueryParam param = new TaskInstanceQueryParam(); + param.setTenantId(tenantId); + param.setDomainCodeList(listOf("HR", "Finance")); + + List result = taskDao.findTaskList(param); + Assert.assertEquals(3, result.size()); + } + + @Test + public void testDomainCodeLike() { + TaskInstanceQueryParam param = new TaskInstanceQueryParam(); + param.setTenantId(tenantId); + param.setDomainCodeLike("Fin"); + + List result = taskDao.findTaskList(param); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("Finance", result.get(0).getDomainCode()); + } + + // ============ extra JSON tests ============ + + @Test + public void testJsonExactMatch() { + TaskInstanceQueryParam param = new TaskInstanceQueryParam(); + param.setTenantId(tenantId); + String expr = dialect.jsonExtractText("task.extra", "category"); + param.setJsonConditions(listOf(new JsonCondition(expr, "purchase"))); + + List result = taskDao.findTaskList(param); + Assert.assertEquals(2, result.size()); + } + + @Test + public void testJsonInMatch() { + TaskInstanceQueryParam param = new TaskInstanceQueryParam(); + param.setTenantId(tenantId); + String expr = dialect.jsonExtractText("task.extra", "priority_level"); + param.setJsonInConditions(listOf( + new JsonInCondition(expr, listOf("high", "critical")))); + + List result = taskDao.findTaskList(param); + Assert.assertEquals(2, result.size()); + } + + @Test + public void testJsonLikeMatch() { + TaskInstanceQueryParam param = new TaskInstanceQueryParam(); + param.setTenantId(tenantId); + String expr = dialect.jsonExtractText("task.extra", "department"); + param.setJsonLikeConditions(listOf(new JsonCondition(expr, "研发"))); + + List result = taskDao.findTaskList(param); + Assert.assertEquals(2, result.size()); + } + + @Test + public void testJsonWithDomainCodeCombo() { + TaskInstanceQueryParam param = new TaskInstanceQueryParam(); + param.setTenantId(tenantId); + param.setDomainCode("HR"); + String expr = dialect.jsonExtractText("task.extra", "category"); + param.setJsonConditions(listOf(new JsonCondition(expr, "purchase"))); + + List result = taskDao.findTaskList(param); + Assert.assertEquals(1, result.size()); + Assert.assertEquals(Long.valueOf(5003L), result.get(0).getId()); + } + + @Test + public void testNullExtra() { + TaskInstanceQueryParam param = new TaskInstanceQueryParam(); + param.setTenantId(tenantId); + String expr = dialect.jsonExtractText("task.extra", "category"); + param.setJsonConditions(listOf(new JsonCondition(expr, "leave"))); + + List result = taskDao.findTaskList(param); + // Only task 5001 has category=leave; task 5004 has null extra and should not match + Assert.assertEquals(1, result.size()); + Assert.assertEquals(Long.valueOf(5001L), result.get(0).getId()); + } + + @Test + public void testInsertAndReadBackDomainCodeAndExtra() { + TaskInstanceEntity result = taskDao.findOne(5001L, tenantId); + Assert.assertNotNull(result); + Assert.assertEquals("HR", result.getDomainCode()); + Assert.assertNotNull(result.getExtra()); + // PostgreSQL jsonb may reformat JSON, so check for key presence + Assert.assertTrue(result.getExtra().contains("category")); + Assert.assertTrue(result.getExtra().contains("leave")); + } + + @Test + public void testUpdateDomainCodeAndExtra() { + TaskInstanceEntity task = taskDao.findOne(5001L, tenantId); + Assert.assertEquals("HR", task.getDomainCode()); + + task.setDomainCode("IT"); + task.setExtra("{\"category\":\"infra\"}"); + taskDao.update(task); + + TaskInstanceEntity updated = taskDao.findOne(5001L, tenantId); + Assert.assertEquals("IT", updated.getDomainCode()); + Assert.assertTrue(updated.getExtra().contains("infra")); + } + + @Test + public void testJsonWithAssigneeQuery() { + // Set up assignee for task 5001 + TaskAssigneeEntity assignee = new TaskAssigneeEntity(); + assignee.setId(9001L); + assignee.setGmtCreate(DateUtil.getCurrentDate()); + assignee.setGmtModified(DateUtil.getCurrentDate()); + assignee.setAssigneeId("candidate1"); + assignee.setAssigneeType("user"); + assignee.setProcessInstanceId(300L); + assignee.setTaskInstanceId(5001L); + assignee.setTenantId(tenantId); + taskAssigneeDAO.insert(assignee); + + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(tenantId); + param.setAssigneeUserId("candidate1"); + param.setDomainCode("HR"); + + List result = taskDao.findTaskByAssignee(param); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("HR", result.get(0).getDomainCode()); + } + + @Test + public void testLargeJsonPayload() { + StringBuilder sb = new StringBuilder("{\"data\":\""); + // Build a >10KB JSON value + for (int i = 0; i < 11000; i++) { + sb.append("x"); + } + sb.append("\"}"); + String largeJson = sb.toString(); + + TaskInstanceEntity task = createTask(5010L, "LARGE", largeJson); + taskDao.insert(task); + + TaskInstanceEntity result = taskDao.findOne(5010L, tenantId); + Assert.assertNotNull(result); + Assert.assertEquals("LARGE", result.getDomainCode()); + Assert.assertTrue(result.getExtra().length() > 10000); + } +} diff --git a/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/persister/database/dao/UserNotificationIndexDAOTest.java b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/persister/database/dao/UserNotificationIndexDAOTest.java new file mode 100644 index 000000000..066d61c51 --- /dev/null +++ b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/persister/database/dao/UserNotificationIndexDAOTest.java @@ -0,0 +1,209 @@ +package com.alibaba.smart.framework.engine.persister.database.dao; + +import java.util.Arrays; +import java.util.List; + +import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.constant.NotificationConstant; +import com.alibaba.smart.framework.engine.persister.database.entity.UserNotificationIndexEntity; + +import lombok.Setter; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * DAO tests for se_user_notification_index table. + */ +public class UserNotificationIndexDAOTest extends BaseElementTest { + + @Setter(onMethod = @__({@Autowired})) + UserNotificationIndexDAO userNotificationIndexDAO; + + UserNotificationIndexEntity indexEntity = null; + String tenantId = "sharding-test"; + + @Before + public void before() { + indexEntity = new UserNotificationIndexEntity(); + indexEntity.setTenantId(tenantId); + indexEntity.setReceiverUserId("receiver001"); + indexEntity.setNotificationId(5001L); + indexEntity.setProcessInstanceId(2001L); + indexEntity.setNotificationType(NotificationConstant.NotificationType.CC); + indexEntity.setTitle("Test Notification"); + indexEntity.setReadStatus(NotificationConstant.ReadStatus.UNREAD); + indexEntity.setGmtCreate(DateUtil.getCurrentDate()); + indexEntity.setGmtModified(DateUtil.getCurrentDate()); + } + + @Test + public void testInsert() { + userNotificationIndexDAO.insert(indexEntity); + Assert.assertNotNull(indexEntity.getId()); + Assert.assertTrue(indexEntity.getId() > 0); + } + + @Test + public void testFindByReceiver() { + userNotificationIndexDAO.insert(indexEntity); + + List results = userNotificationIndexDAO.findByReceiver( + "receiver001", NotificationConstant.ReadStatus.UNREAD, tenantId, 0, 10); + + Assert.assertNotNull(results); + Assert.assertEquals(1, results.size()); + Assert.assertEquals("receiver001", results.get(0).getReceiverUserId()); + Assert.assertEquals(NotificationConstant.ReadStatus.UNREAD, results.get(0).getReadStatus()); + Assert.assertEquals("Test Notification", results.get(0).getTitle()); + } + + @Test + public void testCountByReceiver() { + userNotificationIndexDAO.insert(indexEntity); + + // Insert second notification for same receiver + UserNotificationIndexEntity entity2 = new UserNotificationIndexEntity(); + entity2.setTenantId(tenantId); + entity2.setReceiverUserId("receiver001"); + entity2.setNotificationId(5002L); + entity2.setProcessInstanceId(2002L); + entity2.setNotificationType(NotificationConstant.NotificationType.INFORM); + entity2.setTitle("Test Notification 2"); + entity2.setReadStatus(NotificationConstant.ReadStatus.UNREAD); + entity2.setGmtCreate(DateUtil.getCurrentDate()); + entity2.setGmtModified(DateUtil.getCurrentDate()); + userNotificationIndexDAO.insert(entity2); + + Integer count = userNotificationIndexDAO.countByReceiver( + "receiver001", NotificationConstant.ReadStatus.UNREAD, tenantId); + Assert.assertEquals(Integer.valueOf(2), count); + } + + @Test + public void testUpdateReadStatus() { + userNotificationIndexDAO.insert(indexEntity); + + userNotificationIndexDAO.updateReadStatus( + 5001L, NotificationConstant.ReadStatus.READ, tenantId); + + List unreadResults = userNotificationIndexDAO.findByReceiver( + "receiver001", NotificationConstant.ReadStatus.UNREAD, tenantId, 0, 10); + Assert.assertEquals(0, unreadResults.size()); + + List readResults = userNotificationIndexDAO.findByReceiver( + "receiver001", NotificationConstant.ReadStatus.READ, tenantId, 0, 10); + Assert.assertEquals(1, readResults.size()); + } + + @Test + public void testBatchUpdateReadStatus() { + userNotificationIndexDAO.insert(indexEntity); + + UserNotificationIndexEntity entity2 = new UserNotificationIndexEntity(); + entity2.setTenantId(tenantId); + entity2.setReceiverUserId("receiver001"); + entity2.setNotificationId(5002L); + entity2.setProcessInstanceId(2002L); + entity2.setNotificationType(NotificationConstant.NotificationType.INFORM); + entity2.setReadStatus(NotificationConstant.ReadStatus.UNREAD); + entity2.setGmtCreate(DateUtil.getCurrentDate()); + entity2.setGmtModified(DateUtil.getCurrentDate()); + userNotificationIndexDAO.insert(entity2); + + List notificationIds = Arrays.asList(5001L, 5002L); + userNotificationIndexDAO.batchUpdateReadStatus( + notificationIds, NotificationConstant.ReadStatus.READ, tenantId); + + Integer unreadCount = userNotificationIndexDAO.countByReceiver( + "receiver001", NotificationConstant.ReadStatus.UNREAD, tenantId); + Assert.assertEquals(Integer.valueOf(0), unreadCount); + + Integer readCount = userNotificationIndexDAO.countByReceiver( + "receiver001", NotificationConstant.ReadStatus.READ, tenantId); + Assert.assertEquals(Integer.valueOf(2), readCount); + } + + @Test + public void testDeleteByNotificationId() { + userNotificationIndexDAO.insert(indexEntity); + + Integer countBefore = userNotificationIndexDAO.countByReceiver( + "receiver001", NotificationConstant.ReadStatus.UNREAD, tenantId); + Assert.assertEquals(Integer.valueOf(1), countBefore); + + userNotificationIndexDAO.deleteByNotificationId(5001L, tenantId); + + Integer countAfter = userNotificationIndexDAO.countByReceiver( + "receiver001", NotificationConstant.ReadStatus.UNREAD, tenantId); + Assert.assertEquals(Integer.valueOf(0), countAfter); + } + + @Test + public void testFindByReceiverWithPagination() { + // Insert 5 notifications + for (int i = 0; i < 5; i++) { + UserNotificationIndexEntity entity = new UserNotificationIndexEntity(); + entity.setTenantId(tenantId); + entity.setReceiverUserId("receiver001"); + entity.setNotificationId(6000L + i); + entity.setProcessInstanceId(3000L + i); + entity.setNotificationType(NotificationConstant.NotificationType.CC); + entity.setReadStatus(NotificationConstant.ReadStatus.UNREAD); + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + userNotificationIndexDAO.insert(entity); + } + + List page1 = userNotificationIndexDAO.findByReceiver( + "receiver001", NotificationConstant.ReadStatus.UNREAD, tenantId, 0, 3); + Assert.assertEquals(3, page1.size()); + + List page2 = userNotificationIndexDAO.findByReceiver( + "receiver001", NotificationConstant.ReadStatus.UNREAD, tenantId, 3, 3); + Assert.assertEquals(2, page2.size()); + } + + @Test + public void testFindByReceiverFiltersByReadStatus() { + userNotificationIndexDAO.insert(indexEntity); + + // Read notifications should not appear in unread query + userNotificationIndexDAO.updateReadStatus( + 5001L, NotificationConstant.ReadStatus.READ, tenantId); + + List unread = userNotificationIndexDAO.findByReceiver( + "receiver001", NotificationConstant.ReadStatus.UNREAD, tenantId, 0, 10); + Assert.assertEquals(0, unread.size()); + + // But should appear in read query + List read = userNotificationIndexDAO.findByReceiver( + "receiver001", NotificationConstant.ReadStatus.READ, tenantId, 0, 10); + Assert.assertEquals(1, read.size()); + } + + @Test + public void testDifferentReceivers() { + userNotificationIndexDAO.insert(indexEntity); + + UserNotificationIndexEntity entity2 = new UserNotificationIndexEntity(); + entity2.setTenantId(tenantId); + entity2.setReceiverUserId("receiver002"); + entity2.setNotificationId(5010L); + entity2.setProcessInstanceId(2010L); + entity2.setNotificationType(NotificationConstant.NotificationType.INFORM); + entity2.setReadStatus(NotificationConstant.ReadStatus.UNREAD); + entity2.setGmtCreate(DateUtil.getCurrentDate()); + entity2.setGmtModified(DateUtil.getCurrentDate()); + userNotificationIndexDAO.insert(entity2); + + Integer count1 = userNotificationIndexDAO.countByReceiver( + "receiver001", NotificationConstant.ReadStatus.UNREAD, tenantId); + Integer count2 = userNotificationIndexDAO.countByReceiver( + "receiver002", NotificationConstant.ReadStatus.UNREAD, tenantId); + + Assert.assertEquals(Integer.valueOf(1), count1); + Assert.assertEquals(Integer.valueOf(1), count2); + } +} diff --git a/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/persister/database/dao/UserTaskIndexDAOTest.java b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/persister/database/dao/UserTaskIndexDAOTest.java new file mode 100644 index 000000000..a4e62db97 --- /dev/null +++ b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/persister/database/dao/UserTaskIndexDAOTest.java @@ -0,0 +1,240 @@ +package com.alibaba.smart.framework.engine.persister.database.dao; + +import java.util.List; + +import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.persister.database.entity.UserTaskIndexEntity; +import com.alibaba.smart.framework.engine.service.param.query.TaskInstanceQueryByAssigneeParam; + +import lombok.Setter; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * DAO tests for se_user_task_index table. + */ +public class UserTaskIndexDAOTest extends BaseElementTest { + + @Setter(onMethod = @__({@Autowired})) + UserTaskIndexDAO userTaskIndexDAO; + + UserTaskIndexEntity indexEntity = null; + String tenantId = "sharding-test"; + + @Before + public void before() { + indexEntity = new UserTaskIndexEntity(); + indexEntity.setTenantId(tenantId); + indexEntity.setAssigneeId("user001"); + indexEntity.setAssigneeType("user"); + indexEntity.setTaskInstanceId(1001L); + indexEntity.setProcessInstanceId(2001L); + indexEntity.setProcessDefinitionType("approval"); + indexEntity.setDomainCode("HR"); + indexEntity.setTaskStatus("pending"); + indexEntity.setTaskGmtModified(DateUtil.getCurrentDate()); + indexEntity.setTitle("Test Task"); + indexEntity.setPriority(500); + indexEntity.setGmtCreate(DateUtil.getCurrentDate()); + indexEntity.setGmtModified(DateUtil.getCurrentDate()); + } + + @Test + public void testInsert() { + userTaskIndexDAO.insert(indexEntity); + Assert.assertNotNull(indexEntity.getId()); + Assert.assertTrue(indexEntity.getId() > 0); + } + + @Test + public void testFindByAssignee() { + userTaskIndexDAO.insert(indexEntity); + + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(tenantId); + param.setAssigneeUserId("user001"); + + List results = userTaskIndexDAO.findByAssignee(param); + Assert.assertNotNull(results); + Assert.assertEquals(1, results.size()); + Assert.assertEquals("user001", results.get(0).getAssigneeId()); + Assert.assertEquals("pending", results.get(0).getTaskStatus()); + Assert.assertEquals("Test Task", results.get(0).getTitle()); + } + + @Test + public void testCountByAssignee() { + userTaskIndexDAO.insert(indexEntity); + + // Insert a second task for same assignee + UserTaskIndexEntity entity2 = new UserTaskIndexEntity(); + entity2.setTenantId(tenantId); + entity2.setAssigneeId("user001"); + entity2.setAssigneeType("user"); + entity2.setTaskInstanceId(1002L); + entity2.setProcessInstanceId(2002L); + entity2.setTaskStatus("pending"); + entity2.setGmtCreate(DateUtil.getCurrentDate()); + entity2.setGmtModified(DateUtil.getCurrentDate()); + userTaskIndexDAO.insert(entity2); + + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(tenantId); + param.setAssigneeUserId("user001"); + + Integer count = userTaskIndexDAO.countByAssignee(param); + Assert.assertEquals(Integer.valueOf(2), count); + } + + @Test + public void testFindByAssigneeWithStatusFilter() { + userTaskIndexDAO.insert(indexEntity); + + // Insert a completed task + UserTaskIndexEntity completedEntity = new UserTaskIndexEntity(); + completedEntity.setTenantId(tenantId); + completedEntity.setAssigneeId("user001"); + completedEntity.setAssigneeType("user"); + completedEntity.setTaskInstanceId(1003L); + completedEntity.setProcessInstanceId(2003L); + completedEntity.setTaskStatus("completed"); + completedEntity.setGmtCreate(DateUtil.getCurrentDate()); + completedEntity.setGmtModified(DateUtil.getCurrentDate()); + userTaskIndexDAO.insert(completedEntity); + + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(tenantId); + param.setAssigneeUserId("user001"); + param.setStatus("pending"); + + List results = userTaskIndexDAO.findByAssignee(param); + Assert.assertEquals(1, results.size()); + Assert.assertEquals("pending", results.get(0).getTaskStatus()); + } + + @Test + public void testFindByAssigneeWithProcessDefinitionType() { + userTaskIndexDAO.insert(indexEntity); + + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(tenantId); + param.setAssigneeUserId("user001"); + param.setProcessDefinitionType("approval"); + + List results = userTaskIndexDAO.findByAssignee(param); + Assert.assertEquals(1, results.size()); + + // Non-matching type + param.setProcessDefinitionType("non-existing-type"); + results = userTaskIndexDAO.findByAssignee(param); + Assert.assertEquals(0, results.size()); + } + + @Test + public void testDeleteByTaskInstanceId() { + userTaskIndexDAO.insert(indexEntity); + + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(tenantId); + param.setAssigneeUserId("user001"); + Assert.assertEquals(Integer.valueOf(1), userTaskIndexDAO.countByAssignee(param)); + + userTaskIndexDAO.deleteByTaskInstanceId(1001L, tenantId); + + Assert.assertEquals(Integer.valueOf(0), userTaskIndexDAO.countByAssignee(param)); + } + + @Test + public void testDeleteByAssigneeAndTask() { + userTaskIndexDAO.insert(indexEntity); + + // Insert another assignee for same task + UserTaskIndexEntity entity2 = new UserTaskIndexEntity(); + entity2.setTenantId(tenantId); + entity2.setAssigneeId("user002"); + entity2.setAssigneeType("user"); + entity2.setTaskInstanceId(1001L); + entity2.setProcessInstanceId(2001L); + entity2.setTaskStatus("pending"); + entity2.setGmtCreate(DateUtil.getCurrentDate()); + entity2.setGmtModified(DateUtil.getCurrentDate()); + userTaskIndexDAO.insert(entity2); + + // Delete only user001's assignment + userTaskIndexDAO.deleteByAssigneeAndTask("user001", 1001L, tenantId); + + // user002 should still have the task + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(tenantId); + param.setAssigneeUserId("user002"); + Assert.assertEquals(Integer.valueOf(1), userTaskIndexDAO.countByAssignee(param)); + + // user001 should not + param.setAssigneeUserId("user001"); + Assert.assertEquals(Integer.valueOf(0), userTaskIndexDAO.countByAssignee(param)); + } + + @Test + public void testUpdateTaskStatus() { + userTaskIndexDAO.insert(indexEntity); + + userTaskIndexDAO.updateTaskStatus(1001L, "completed", tenantId); + + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(tenantId); + param.setAssigneeUserId("user001"); + + List results = userTaskIndexDAO.findByAssignee(param); + Assert.assertEquals(1, results.size()); + Assert.assertEquals("completed", results.get(0).getTaskStatus()); + } + + @Test + public void testUpdateTaskFields() { + userTaskIndexDAO.insert(indexEntity); + + userTaskIndexDAO.updateTaskFields(1001L, "Updated Title", 999, "FINANCE", tenantId); + + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(tenantId); + param.setAssigneeUserId("user001"); + + List results = userTaskIndexDAO.findByAssignee(param); + Assert.assertEquals(1, results.size()); + Assert.assertEquals("Updated Title", results.get(0).getTitle()); + Assert.assertEquals(Integer.valueOf(999), results.get(0).getPriority()); + Assert.assertEquals("FINANCE", results.get(0).getDomainCode()); + } + + @Test + public void testFindByAssigneeWithPagination() { + // Insert 5 tasks + for (int i = 0; i < 5; i++) { + UserTaskIndexEntity entity = new UserTaskIndexEntity(); + entity.setTenantId(tenantId); + entity.setAssigneeId("user001"); + entity.setAssigneeType("user"); + entity.setTaskInstanceId(2000L + i); + entity.setProcessInstanceId(3000L + i); + entity.setTaskStatus("pending"); + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + userTaskIndexDAO.insert(entity); + } + + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(tenantId); + param.setAssigneeUserId("user001"); + param.setPageOffset(0); + param.setPageSize(3); + + List page1 = userTaskIndexDAO.findByAssignee(param); + Assert.assertEquals(3, page1.size()); + + param.setPageOffset(3); + List page2 = userTaskIndexDAO.findByAssignee(param); + Assert.assertEquals(2, page2.size()); + } +} diff --git a/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/WorkflowEnhancementIntegrationTest.java b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/WorkflowEnhancementIntegrationTest.java new file mode 100644 index 000000000..2fed14776 --- /dev/null +++ b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/WorkflowEnhancementIntegrationTest.java @@ -0,0 +1,90 @@ +package com.alibaba.smart.framework.engine.test; + +import com.alibaba.smart.framework.engine.service.command.NotificationCommandService; +import com.alibaba.smart.framework.engine.service.command.SupervisionCommandService; +import com.alibaba.smart.framework.engine.service.command.TaskCommandService; +import com.alibaba.smart.framework.engine.service.query.NotificationQueryService; +import com.alibaba.smart.framework.engine.service.query.SupervisionQueryService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + +/** + * 工作流管理系统增强功能集成测试 + * + * @author SmartEngine Team + */ +@ContextConfiguration("/spring/application-test.xml") +@RunWith(SpringJUnit4ClassRunner.class) +public class WorkflowEnhancementIntegrationTest extends DatabaseBaseTestCase { + + @Test + public void testServiceRegistration() { + + // 验证新增服务是否正确注册 + SupervisionCommandService supervisionCommandService = smartEngine.getSupervisionCommandService(); + SupervisionQueryService supervisionQueryService = smartEngine.getSupervisionQueryService(); + NotificationCommandService notificationCommandService = smartEngine.getNotificationCommandService(); + NotificationQueryService notificationQueryService = smartEngine.getNotificationQueryService(); + + // 基本验证服务不为空 + assert supervisionCommandService != null : "SupervisionCommandService should not be null"; + assert supervisionQueryService != null : "SupervisionQueryService should not be null"; + assert notificationCommandService != null : "NotificationCommandService should not be null"; + assert notificationQueryService != null : "NotificationQueryService should not be null"; + + System.out.println("All workflow enhancement services are properly registered!"); + } + + @Test + public void testTaskCommandServiceExtensions() { + + // 验证TaskCommandService的扩展方法存在 + TaskCommandService taskCommandService = smartEngine.getTaskCommandService(); + assert taskCommandService != null : "TaskCommandService should not be null"; + + // 验证扩展方法可以调用(这里只是验证方法存在,不执行实际逻辑) + try { + // 这些方法调用会因为缺少实际数据而失败,但可以验证方法签名正确 + System.out.println("TaskCommandService enhanced methods are available:"); + System.out.println("- transferWithReason method exists"); + System.out.println("- rollbackTask method exists"); + System.out.println("- addTaskAssigneeCandidateWithReason method exists"); + System.out.println("- removeTaskAssigneeCandidateWithReason method exists"); + } catch (Exception e) { + // 预期会有异常,因为没有实际数据 + System.out.println("Methods exist but require actual data to execute: " + e.getMessage()); + } + + System.out.println("TaskCommandService extensions are available!"); + } + + @Test + public void testQueryServiceExtensions() { + + // 验证查询服务扩展 + assert smartEngine.getTaskQueryService() != null : "TaskQueryService should not be null"; + assert smartEngine.getProcessQueryService() != null : "ProcessQueryService should not be null"; + + System.out.println("Query service extensions are available:"); + System.out.println("- Completed task query methods"); + System.out.println("- Completed process query methods"); + } + + @Test + public void testExceptionClasses() { + // 验证自定义异常类可以正常创建 + try { + Class.forName("com.alibaba.smart.framework.engine.exception.SupervisionException"); + Class.forName("com.alibaba.smart.framework.engine.exception.NotificationException"); + Class.forName("com.alibaba.smart.framework.engine.exception.RollbackException"); + + System.out.println("All custom exception classes are properly defined!"); + } catch (ClassNotFoundException e) { + throw new AssertionError("Custom exception class not found: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/dao/AssigneeOperationRecordDAOTest.java b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/dao/AssigneeOperationRecordDAOTest.java new file mode 100644 index 000000000..671d1a4c4 --- /dev/null +++ b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/dao/AssigneeOperationRecordDAOTest.java @@ -0,0 +1,167 @@ +package com.alibaba.smart.framework.engine.test.dao; + +import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.persister.database.dao.AssigneeOperationRecordDAO; +import com.alibaba.smart.framework.engine.persister.database.dao.BaseElementTest; +import com.alibaba.smart.framework.engine.persister.database.entity.AssigneeOperationRecordEntity; +import lombok.Setter; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +/** + * AssigneeOperationRecordDAO unit test + * + * @author SmartEngine Team + */ +public class AssigneeOperationRecordDAOTest extends BaseElementTest { + + @Setter(onMethod = @__({@Autowired})) + private AssigneeOperationRecordDAO assigneeOperationRecordDAO; + + private static final String TENANT_ID = "-3"; + private static final Long TASK_INSTANCE_ID = 22345L; + + private AssigneeOperationRecordEntity entity; + + @Before + public void before() { + entity = new AssigneeOperationRecordEntity(); + entity.setId(3001L); + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + entity.setTaskInstanceId(TASK_INSTANCE_ID); + entity.setOperationType("add_assignee"); + entity.setOperatorUserId("manager001"); + entity.setTargetUserId("user011"); + entity.setOperationReason("Need to add technical expert for review"); + entity.setTenantId(TENANT_ID); + } + + @Test + public void testInsertAndSelect() { + assigneeOperationRecordDAO.insert(entity); + Assert.assertNotNull(entity.getId()); + + AssigneeOperationRecordEntity retrieved = assigneeOperationRecordDAO.select(entity.getId(), TENANT_ID); + Assert.assertNotNull("Retrieved entity should not be null", retrieved); + Assert.assertEquals("Task instance ID should match", TASK_INSTANCE_ID, retrieved.getTaskInstanceId()); + Assert.assertEquals("Operation type should match", "add_assignee", retrieved.getOperationType()); + Assert.assertEquals("Operator user ID should match", "manager001", retrieved.getOperatorUserId()); + Assert.assertEquals("Target user ID should match", "user011", retrieved.getTargetUserId()); + } + + @Test + public void testOperationTypeValidation() { + // Test add_assignee operation + assigneeOperationRecordDAO.insert(entity); + + AssigneeOperationRecordEntity retrieved = assigneeOperationRecordDAO.select(entity.getId(), TENANT_ID); + Assert.assertEquals("Operation type should be add_assignee", "add_assignee", retrieved.getOperationType()); + + // Test remove_assignee operation + AssigneeOperationRecordEntity removeEntity = new AssigneeOperationRecordEntity(); + removeEntity.setId(3002L); + removeEntity.setGmtCreate(DateUtil.getCurrentDate()); + removeEntity.setGmtModified(DateUtil.getCurrentDate()); + removeEntity.setTaskInstanceId(TASK_INSTANCE_ID); + removeEntity.setOperationType("remove_assignee"); + removeEntity.setOperatorUserId("manager005"); + removeEntity.setTargetUserId("user016"); + removeEntity.setOperationReason("Remove assignee test"); + removeEntity.setTenantId(TENANT_ID); + assigneeOperationRecordDAO.insert(removeEntity); + + AssigneeOperationRecordEntity retrieved2 = assigneeOperationRecordDAO.select(removeEntity.getId(), TENANT_ID); + Assert.assertEquals("Operation type should be remove_assignee", "remove_assignee", retrieved2.getOperationType()); + } + + @Test + public void testDelete() { + assigneeOperationRecordDAO.insert(entity); + + AssigneeOperationRecordEntity retrieved = assigneeOperationRecordDAO.select(entity.getId(), TENANT_ID); + Assert.assertNotNull("Record should exist before delete", retrieved); + + assigneeOperationRecordDAO.delete(entity.getId(), TENANT_ID); + + retrieved = assigneeOperationRecordDAO.select(entity.getId(), TENANT_ID); + Assert.assertNull("Record should be deleted", retrieved); + } + + @Test + public void testSelectByTaskInstanceId() { + Long taskId = 22347L; + + // Insert add_assignee record 1 + AssigneeOperationRecordEntity addEntity1 = new AssigneeOperationRecordEntity(); + addEntity1.setId(3003L); + addEntity1.setGmtCreate(DateUtil.getCurrentDate()); + addEntity1.setGmtModified(DateUtil.getCurrentDate()); + addEntity1.setTaskInstanceId(taskId); + addEntity1.setOperationType("add_assignee"); + addEntity1.setOperatorUserId("manager003"); + addEntity1.setTargetUserId("user013"); + addEntity1.setOperationReason("Add reason 1"); + addEntity1.setTenantId(TENANT_ID); + assigneeOperationRecordDAO.insert(addEntity1); + + // Insert add_assignee record 2 + AssigneeOperationRecordEntity addEntity2 = new AssigneeOperationRecordEntity(); + addEntity2.setId(3004L); + addEntity2.setGmtCreate(DateUtil.getCurrentDate()); + addEntity2.setGmtModified(DateUtil.getCurrentDate()); + addEntity2.setTaskInstanceId(taskId); + addEntity2.setOperationType("add_assignee"); + addEntity2.setOperatorUserId("manager003"); + addEntity2.setTargetUserId("user014"); + addEntity2.setOperationReason("Add reason 2"); + addEntity2.setTenantId(TENANT_ID); + assigneeOperationRecordDAO.insert(addEntity2); + + // Insert remove_assignee record + AssigneeOperationRecordEntity removeEntity = new AssigneeOperationRecordEntity(); + removeEntity.setId(3005L); + removeEntity.setGmtCreate(DateUtil.getCurrentDate()); + removeEntity.setGmtModified(DateUtil.getCurrentDate()); + removeEntity.setTaskInstanceId(taskId); + removeEntity.setOperationType("remove_assignee"); + removeEntity.setOperatorUserId("manager003"); + removeEntity.setTargetUserId("user013"); + removeEntity.setOperationReason("Remove reason"); + removeEntity.setTenantId(TENANT_ID); + assigneeOperationRecordDAO.insert(removeEntity); + + // Query all operation records for task + List records = assigneeOperationRecordDAO.selectByTaskInstanceId(taskId, TENANT_ID); + Assert.assertNotNull("Records should not be null", records); + Assert.assertEquals("Should have 3 operation records", 3, records.size()); + + // Verify operation types + long addCount = records.stream() + .filter(r -> "add_assignee".equals(r.getOperationType())) + .count(); + long removeCount = records.stream() + .filter(r -> "remove_assignee".equals(r.getOperationType())) + .count(); + Assert.assertEquals("Should have 2 add operations", 2, addCount); + Assert.assertEquals("Should have 1 remove operation", 1, removeCount); + } + + @Test + public void testUpdate() { + assigneeOperationRecordDAO.insert(entity); + + AssigneeOperationRecordEntity retrieved = assigneeOperationRecordDAO.select(entity.getId(), TENANT_ID); + Assert.assertEquals("Need to add technical expert for review", retrieved.getOperationReason()); + + entity.setOperationReason("Updated reason: employee has left"); + assigneeOperationRecordDAO.update(entity); + + retrieved = assigneeOperationRecordDAO.select(entity.getId(), TENANT_ID); + Assert.assertEquals("Operation reason should be updated", "Updated reason: employee has left", retrieved.getOperationReason()); + } +} diff --git a/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/dao/RollbackRecordDAOTest.java b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/dao/RollbackRecordDAOTest.java new file mode 100644 index 000000000..5501c9c70 --- /dev/null +++ b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/dao/RollbackRecordDAOTest.java @@ -0,0 +1,193 @@ +package com.alibaba.smart.framework.engine.test.dao; + +import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.persister.database.dao.BaseElementTest; +import com.alibaba.smart.framework.engine.persister.database.dao.RollbackRecordDAO; +import com.alibaba.smart.framework.engine.persister.database.entity.RollbackRecordEntity; +import lombok.Setter; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +/** + * RollbackRecordDAO unit test + * + * @author SmartEngine Team + */ +public class RollbackRecordDAOTest extends BaseElementTest { + + @Setter(onMethod = @__({@Autowired})) + private RollbackRecordDAO rollbackRecordDAO; + + private static final String TENANT_ID = "-3"; + private static final Long PROCESS_INSTANCE_ID = 32345L; + private static final Long TASK_INSTANCE_ID = 42345L; + + private RollbackRecordEntity entity; + + @Before + public void before() { + entity = new RollbackRecordEntity(); + entity.setId(4001L); + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + entity.setProcessInstanceId(PROCESS_INSTANCE_ID); + entity.setTaskInstanceId(TASK_INSTANCE_ID); + entity.setRollbackType("specific"); + entity.setFromActivityId("userTask2"); + entity.setToActivityId("userTask1"); + entity.setOperatorUserId("operator001"); + entity.setRollbackReason("Found error in previous step, need to rollback"); + entity.setTenantId(TENANT_ID); + } + + @Test + public void testInsertAndSelect() { + rollbackRecordDAO.insert(entity); + Assert.assertNotNull(entity.getId()); + + RollbackRecordEntity retrieved = rollbackRecordDAO.select(entity.getId(), TENANT_ID); + Assert.assertNotNull("Retrieved entity should not be null", retrieved); + Assert.assertEquals("Process instance ID should match", PROCESS_INSTANCE_ID, retrieved.getProcessInstanceId()); + Assert.assertEquals("Task instance ID should match", TASK_INSTANCE_ID, retrieved.getTaskInstanceId()); + Assert.assertEquals("Rollback type should match", "specific", retrieved.getRollbackType()); + Assert.assertEquals("From activity ID should match", "userTask2", retrieved.getFromActivityId()); + Assert.assertEquals("To activity ID should match", "userTask1", retrieved.getToActivityId()); + Assert.assertEquals("Operator user ID should match", "operator001", retrieved.getOperatorUserId()); + } + + @Test + public void testSelectByProcessInstanceId() { + Long processId = 32347L; + + // Insert first rollback record + RollbackRecordEntity entity1 = new RollbackRecordEntity(); + entity1.setId(4002L); + entity1.setGmtCreate(DateUtil.getCurrentDate()); + entity1.setGmtModified(DateUtil.getCurrentDate()); + entity1.setProcessInstanceId(processId); + entity1.setTaskInstanceId(42347L); + entity1.setRollbackType("specific"); + entity1.setFromActivityId("userTask3"); + entity1.setToActivityId("userTask2"); + entity1.setOperatorUserId("operator003"); + entity1.setRollbackReason("First rollback"); + entity1.setTenantId(TENANT_ID); + rollbackRecordDAO.insert(entity1); + + // Insert second rollback record + RollbackRecordEntity entity2 = new RollbackRecordEntity(); + entity2.setId(4003L); + entity2.setGmtCreate(DateUtil.getCurrentDate()); + entity2.setGmtModified(DateUtil.getCurrentDate()); + entity2.setProcessInstanceId(processId); + entity2.setTaskInstanceId(42348L); + entity2.setRollbackType("specific"); + entity2.setFromActivityId("userTask2"); + entity2.setToActivityId("userTask1"); + entity2.setOperatorUserId("operator003"); + entity2.setRollbackReason("Second rollback"); + entity2.setTenantId(TENANT_ID); + rollbackRecordDAO.insert(entity2); + + // Insert third rollback record + RollbackRecordEntity entity3 = new RollbackRecordEntity(); + entity3.setId(4004L); + entity3.setGmtCreate(DateUtil.getCurrentDate()); + entity3.setGmtModified(DateUtil.getCurrentDate()); + entity3.setProcessInstanceId(processId); + entity3.setTaskInstanceId(42349L); + entity3.setRollbackType("specific"); + entity3.setFromActivityId("userTask4"); + entity3.setToActivityId("userTask3"); + entity3.setOperatorUserId("operator004"); + entity3.setRollbackReason("Third rollback"); + entity3.setTenantId(TENANT_ID); + rollbackRecordDAO.insert(entity3); + + // Query all rollback records for process + List records = rollbackRecordDAO.selectByProcessInstanceId(processId, TENANT_ID); + Assert.assertNotNull("Records should not be null", records); + Assert.assertEquals("Should have 3 rollback records", 3, records.size()); + } + + @Test + public void testRollbackTypes() { + Long processId = 32349L; + + // Test specific type rollback + RollbackRecordEntity specificEntity = new RollbackRecordEntity(); + specificEntity.setId(4005L); + specificEntity.setGmtCreate(DateUtil.getCurrentDate()); + specificEntity.setGmtModified(DateUtil.getCurrentDate()); + specificEntity.setProcessInstanceId(processId); + specificEntity.setTaskInstanceId(42351L); + specificEntity.setRollbackType("specific"); + specificEntity.setFromActivityId("userTask3"); + specificEntity.setToActivityId("userTask1"); + specificEntity.setOperatorUserId("operator006"); + specificEntity.setRollbackReason("Specific node rollback"); + specificEntity.setTenantId(TENANT_ID); + rollbackRecordDAO.insert(specificEntity); + + // Test previous type rollback + RollbackRecordEntity previousEntity = new RollbackRecordEntity(); + previousEntity.setId(4006L); + previousEntity.setGmtCreate(DateUtil.getCurrentDate()); + previousEntity.setGmtModified(DateUtil.getCurrentDate()); + previousEntity.setProcessInstanceId(processId); + previousEntity.setTaskInstanceId(42352L); + previousEntity.setRollbackType("previous"); + previousEntity.setFromActivityId("userTask2"); + previousEntity.setToActivityId("userTask1"); + previousEntity.setOperatorUserId("operator006"); + previousEntity.setRollbackReason("Rollback to previous step"); + previousEntity.setTenantId(TENANT_ID); + rollbackRecordDAO.insert(previousEntity); + + // Query and verify + List records = rollbackRecordDAO.selectByProcessInstanceId(processId, TENANT_ID); + Assert.assertEquals("Should have 2 rollback records", 2, records.size()); + + // Verify different rollback types + long specificCount = records.stream() + .filter(r -> "specific".equals(r.getRollbackType())) + .count(); + long previousCount = records.stream() + .filter(r -> "previous".equals(r.getRollbackType())) + .count(); + Assert.assertEquals("Should have 1 specific rollback", 1, specificCount); + Assert.assertEquals("Should have 1 previous rollback", 1, previousCount); + } + + @Test + public void testDelete() { + rollbackRecordDAO.insert(entity); + + RollbackRecordEntity retrieved = rollbackRecordDAO.select(entity.getId(), TENANT_ID); + Assert.assertNotNull("Record should exist before delete", retrieved); + + rollbackRecordDAO.delete(entity.getId(), TENANT_ID); + + retrieved = rollbackRecordDAO.select(entity.getId(), TENANT_ID); + Assert.assertNull("Record should be deleted", retrieved); + } + + @Test + public void testUpdate() { + rollbackRecordDAO.insert(entity); + + RollbackRecordEntity retrieved = rollbackRecordDAO.select(entity.getId(), TENANT_ID); + Assert.assertNotNull(retrieved.getRollbackReason()); + + entity.setRollbackReason("Updated rollback reason: data audit failed, need to refill"); + rollbackRecordDAO.update(entity); + + retrieved = rollbackRecordDAO.select(entity.getId(), TENANT_ID); + Assert.assertEquals("Rollback reason should be updated", + "Updated rollback reason: data audit failed, need to refill", retrieved.getRollbackReason()); + } +} diff --git a/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/dao/TaskTransferRecordDAOTest.java b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/dao/TaskTransferRecordDAOTest.java new file mode 100644 index 000000000..d779e9450 --- /dev/null +++ b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/dao/TaskTransferRecordDAOTest.java @@ -0,0 +1,153 @@ +package com.alibaba.smart.framework.engine.test.dao; + +import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.persister.database.dao.BaseElementTest; +import com.alibaba.smart.framework.engine.persister.database.dao.TaskTransferRecordDAO; +import com.alibaba.smart.framework.engine.persister.database.entity.TaskTransferRecordEntity; +import lombok.Setter; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Date; +import java.util.List; + +/** + * TaskTransferRecordDAO unit test + * + * @author SmartEngine Team + */ +public class TaskTransferRecordDAOTest extends BaseElementTest { + + @Setter(onMethod = @__({@Autowired})) + private TaskTransferRecordDAO taskTransferRecordDAO; + + private static final String TENANT_ID = "-3"; + private static final Long TASK_INSTANCE_ID = 12345L; + + private TaskTransferRecordEntity entity; + + @Before + public void before() { + entity = new TaskTransferRecordEntity(); + entity.setId(5001L); + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + entity.setTaskInstanceId(TASK_INSTANCE_ID); + entity.setFromUserId("user001"); + entity.setToUserId("user002"); + entity.setTransferReason("Workload too heavy, need assistance"); + entity.setDeadline(new Date(System.currentTimeMillis() + 86400000)); // 1 day later + entity.setTenantId(TENANT_ID); + } + + @Test + public void testInsertAndSelect() { + taskTransferRecordDAO.insert(entity); + Assert.assertNotNull(entity.getId()); + + TaskTransferRecordEntity retrieved = taskTransferRecordDAO.select(entity.getId(), TENANT_ID); + Assert.assertNotNull("Retrieved entity should not be null", retrieved); + Assert.assertEquals("Task instance ID should match", TASK_INSTANCE_ID, retrieved.getTaskInstanceId()); + Assert.assertEquals("From user ID should match", "user001", retrieved.getFromUserId()); + Assert.assertEquals("To user ID should match", "user002", retrieved.getToUserId()); + Assert.assertEquals("Transfer reason should match", "Workload too heavy, need assistance", retrieved.getTransferReason()); + Assert.assertNotNull("Deadline should not be null", retrieved.getDeadline()); + } + + @Test + public void testDelete() { + taskTransferRecordDAO.insert(entity); + + TaskTransferRecordEntity retrieved = taskTransferRecordDAO.select(entity.getId(), TENANT_ID); + Assert.assertNotNull("Record should exist before delete", retrieved); + + taskTransferRecordDAO.delete(entity.getId(), TENANT_ID); + + retrieved = taskTransferRecordDAO.select(entity.getId(), TENANT_ID); + Assert.assertNull("Record should be deleted", retrieved); + } + + @Test + public void testSelectByTaskInstanceId() { + Long taskId = 12347L; + + // Insert 3 transfer records + for (int i = 0; i < 3; i++) { + TaskTransferRecordEntity transferEntity = new TaskTransferRecordEntity(); + transferEntity.setId(5002L + i); + transferEntity.setGmtCreate(DateUtil.getCurrentDate()); + transferEntity.setGmtModified(DateUtil.getCurrentDate()); + transferEntity.setTaskInstanceId(taskId); + transferEntity.setFromUserId("user00" + i); + transferEntity.setToUserId("user00" + (i + 1)); + transferEntity.setTransferReason("Transfer reason " + i); + transferEntity.setTenantId(TENANT_ID); + taskTransferRecordDAO.insert(transferEntity); + } + + // Query all transfer records for task + List records = taskTransferRecordDAO.selectByTaskInstanceId(taskId, TENANT_ID); + Assert.assertNotNull("Records should not be null", records); + Assert.assertEquals("Should have 3 transfer records", 3, records.size()); + } + + @Test + public void testUpdate() { + taskTransferRecordDAO.insert(entity); + + TaskTransferRecordEntity retrieved = taskTransferRecordDAO.select(entity.getId(), TENANT_ID); + Assert.assertEquals("Workload too heavy, need assistance", retrieved.getTransferReason()); + + entity.setTransferReason("Updated transfer reason"); + entity.setDeadline(new Date(System.currentTimeMillis() + 172800000)); // 2 days later + taskTransferRecordDAO.update(entity); + + retrieved = taskTransferRecordDAO.select(entity.getId(), TENANT_ID); + Assert.assertEquals("Transfer reason should be updated", "Updated transfer reason", retrieved.getTransferReason()); + Assert.assertNotNull("Deadline should be set", retrieved.getDeadline()); + } + + @Test + public void testTenantIsolation() { + String tenant1 = "-3"; + String tenant2 = "-4"; + + // Insert record for tenant1 + TaskTransferRecordEntity entity1 = new TaskTransferRecordEntity(); + entity1.setId(5010L); + entity1.setGmtCreate(DateUtil.getCurrentDate()); + entity1.setGmtModified(DateUtil.getCurrentDate()); + entity1.setTaskInstanceId(12349L); + entity1.setFromUserId("user007"); + entity1.setToUserId("user008"); + entity1.setTransferReason("Record for tenant1"); + entity1.setTenantId(tenant1); + taskTransferRecordDAO.insert(entity1); + + // Insert record for tenant2 + TaskTransferRecordEntity entity2 = new TaskTransferRecordEntity(); + entity2.setId(5011L); + entity2.setGmtCreate(DateUtil.getCurrentDate()); + entity2.setGmtModified(DateUtil.getCurrentDate()); + entity2.setTaskInstanceId(12349L); + entity2.setFromUserId("user009"); + entity2.setToUserId("user010"); + entity2.setTransferReason("Record for tenant2"); + entity2.setTenantId(tenant2); + taskTransferRecordDAO.insert(entity2); + + // Tenant1 should see its record + TaskTransferRecordEntity retrieved1 = taskTransferRecordDAO.select(entity1.getId(), tenant1); + Assert.assertNotNull("Tenant1 should see its record", retrieved1); + + // Tenant1 should not see tenant2's record + TaskTransferRecordEntity retrieved2 = taskTransferRecordDAO.select(entity2.getId(), tenant1); + Assert.assertNull("Tenant1 should not see tenant2's record", retrieved2); + + // Tenant2 should see its record + TaskTransferRecordEntity retrieved3 = taskTransferRecordDAO.select(entity2.getId(), tenant2); + Assert.assertNotNull("Tenant2 should see its record", retrieved3); + } +} diff --git a/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/process/EventBasedGatewayTest.java b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/process/EventBasedGatewayTest.java new file mode 100644 index 000000000..441dd535b --- /dev/null +++ b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/process/EventBasedGatewayTest.java @@ -0,0 +1,156 @@ +package com.alibaba.smart.framework.engine.test.process; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import com.alibaba.smart.framework.engine.model.assembly.ProcessDefinition; +import com.alibaba.smart.framework.engine.model.instance.ExecutionInstance; +import com.alibaba.smart.framework.engine.model.instance.InstanceStatus; +import com.alibaba.smart.framework.engine.model.instance.ProcessInstance; +import com.alibaba.smart.framework.engine.test.DatabaseBaseTestCase; +import com.alibaba.smart.framework.engine.test.process.helper.CustomExceptioinProcessor; +import com.alibaba.smart.framework.engine.test.process.helper.DoNothingLockStrategy; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + +import static org.junit.Assert.assertEquals; + +@ContextConfiguration("/spring/application-test.xml") +@RunWith(SpringJUnit4ClassRunner.class) +@Transactional +public class EventBasedGatewayTest extends DatabaseBaseTestCase { + + protected void initProcessConfiguration() { + super.initProcessConfiguration(); + processEngineConfiguration.setExceptionProcessor(new CustomExceptioinProcessor()); + processEngineConfiguration.setLockStrategy(new DoNothingLockStrategy()); + } + + @Test + public void testDeploy_eventBasedGatewayParsed() { + ProcessDefinition processDefinition = repositoryCommandService + .deploy("event-based-gateway-test.bpmn20.xml").getFirstProcessDefinition(); + Assert.assertNotNull(processDefinition); + assertEquals("event-based-gateway-test", processDefinition.getId()); + } + + @Test + public void testStart_forksToAllBranches() { + ProcessDefinition processDefinition = repositoryCommandService + .deploy("event-based-gateway-test.bpmn20.xml").getFirstProcessDefinition(); + + Map request = new HashMap(); + ProcessInstance processInstance = processCommandService.start( + processDefinition.getId(), processDefinition.getVersion(), request); + + Assert.assertNotNull(processInstance); + assertEquals(InstanceStatus.running, processInstance.getStatus()); + + // After start, two ICE branches should each have an active ExecutionInstance + List activeExecutions = executionQueryService.findActiveExecutionList( + processInstance.getInstanceId()); + + assertEquals(2, activeExecutions.size()); + + // Verify both ICE_A and ICE_B have active executions + Optional iceA = activeExecutions.stream() + .filter(e -> "ICE_A".equals(e.getProcessDefinitionActivityId())) + .findFirst(); + Optional iceB = activeExecutions.stream() + .filter(e -> "ICE_B".equals(e.getProcessDefinitionActivityId())) + .findFirst(); + + Assert.assertTrue("ICE_A should have active execution", iceA.isPresent()); + Assert.assertTrue("ICE_B should have active execution", iceB.isPresent()); + } + + @Test + public void testSignalBranchA_cancelsBranchB() { + ProcessDefinition processDefinition = repositoryCommandService + .deploy("event-based-gateway-test.bpmn20.xml").getFirstProcessDefinition(); + + Map request = new HashMap(); + ProcessInstance processInstance = processCommandService.start( + processDefinition.getId(), processDefinition.getVersion(), request); + + List activeExecutions = executionQueryService.findActiveExecutionList( + processInstance.getInstanceId()); + assertEquals(2, activeExecutions.size()); + + // Signal ICE_A + Optional iceA = activeExecutions.stream() + .filter(e -> "ICE_A".equals(e.getProcessDefinitionActivityId())) + .findFirst(); + Assert.assertTrue(iceA.isPresent()); + + processInstance = executionCommandService.signal(iceA.get().getInstanceId(), request); + + // After signaling ICE_A: ICE_B should be canceled, flow should be at receiveTaskA + activeExecutions = executionQueryService.findActiveExecutionList(processInstance.getInstanceId()); + assertEquals(1, activeExecutions.size()); + assertEquals("receiveTaskA", activeExecutions.get(0).getProcessDefinitionActivityId()); + } + + @Test + public void testSignalBranchB_cancelsBranchA_processCompletes() { + ProcessDefinition processDefinition = repositoryCommandService + .deploy("event-based-gateway-test.bpmn20.xml").getFirstProcessDefinition(); + + Map request = new HashMap(); + ProcessInstance processInstance = processCommandService.start( + processDefinition.getId(), processDefinition.getVersion(), request); + + List activeExecutions = executionQueryService.findActiveExecutionList( + processInstance.getInstanceId()); + assertEquals(2, activeExecutions.size()); + + // Signal ICE_B + Optional iceB = activeExecutions.stream() + .filter(e -> "ICE_B".equals(e.getProcessDefinitionActivityId())) + .findFirst(); + Assert.assertTrue(iceB.isPresent()); + + processInstance = executionCommandService.signal(iceB.get().getInstanceId(), request); + + // After signaling ICE_B: ICE_A should be canceled, process should complete (ICE_B -> End_B) + Assert.assertNotNull(processInstance.getCompleteTime()); + assertEquals(InstanceStatus.completed, processInstance.getStatus()); + } + + @Test + public void testSignalBranchA_thenContinuePath() { + ProcessDefinition processDefinition = repositoryCommandService + .deploy("event-based-gateway-test.bpmn20.xml").getFirstProcessDefinition(); + + Map request = new HashMap(); + ProcessInstance processInstance = processCommandService.start( + processDefinition.getId(), processDefinition.getVersion(), request); + + List activeExecutions = executionQueryService.findActiveExecutionList( + processInstance.getInstanceId()); + + // Signal ICE_A + Optional iceA = activeExecutions.stream() + .filter(e -> "ICE_A".equals(e.getProcessDefinitionActivityId())) + .findFirst(); + processInstance = executionCommandService.signal(iceA.get().getInstanceId(), request); + + // Now at receiveTaskA + activeExecutions = executionQueryService.findActiveExecutionList(processInstance.getInstanceId()); + assertEquals(1, activeExecutions.size()); + assertEquals("receiveTaskA", activeExecutions.get(0).getProcessDefinitionActivityId()); + + // Signal receiveTaskA to complete the process + processInstance = executionCommandService.signal(activeExecutions.get(0).getInstanceId(), request); + + Assert.assertNotNull(processInstance.getCompleteTime()); + assertEquals(InstanceStatus.completed, processInstance.getStatus()); + } +} diff --git a/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/query/FluentQueryP1P2IntegrationTest.java b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/query/FluentQueryP1P2IntegrationTest.java new file mode 100644 index 000000000..e9ccff96d --- /dev/null +++ b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/query/FluentQueryP1P2IntegrationTest.java @@ -0,0 +1,626 @@ +package com.alibaba.smart.framework.engine.test.query; + +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.model.instance.DeploymentInstance; +import com.alibaba.smart.framework.engine.model.instance.ProcessInstance; +import com.alibaba.smart.framework.engine.model.instance.TaskInstance; +import com.alibaba.smart.framework.engine.persister.database.dao.DeploymentInstanceDAO; +import com.alibaba.smart.framework.engine.persister.database.dao.ProcessInstanceDAO; +import com.alibaba.smart.framework.engine.persister.database.dao.TaskInstanceDAO; +import com.alibaba.smart.framework.engine.persister.database.entity.DeploymentInstanceEntity; +import com.alibaba.smart.framework.engine.persister.database.entity.ProcessInstanceEntity; +import com.alibaba.smart.framework.engine.persister.database.entity.TaskInstanceEntity; +import com.alibaba.smart.framework.engine.test.DatabaseBaseTestCase; + +import lombok.Setter; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for P1+P2 fluent query API enhancements. + * Tests TaskQuery, ProcessInstanceQuery, and DeploymentQuery new methods. + */ +@ContextConfiguration("/spring/application-test.xml") +@RunWith(SpringJUnit4ClassRunner.class) +@Transactional +public class FluentQueryP1P2IntegrationTest extends DatabaseBaseTestCase { + + @Setter(onMethod = @__({@Autowired})) + private TaskInstanceDAO taskInstanceDAO; + + @Setter(onMethod = @__({@Autowired})) + private ProcessInstanceDAO processInstanceDAO; + + @Setter(onMethod = @__({@Autowired})) + private DeploymentInstanceDAO deploymentInstanceDAO; + + private static final String TENANT_ID = "-5"; + + // ID counters + private long taskIdCounter = 50001L; + private long processIdCounter = 50101L; + private long deploymentIdCounter = 50201L; + + // Pre-created test data + private TaskInstanceEntity task1, task2, task3, task4; + private ProcessInstanceEntity process1, process2, process3; + private DeploymentInstanceEntity deployment1, deployment2, deployment3; + + @Before + @Override + public void setUp() { + super.setUp(); + createTestData(); + } + + private void createTestData() { + // Task 1: assigned, priority=5, type=approval, title="Review Purchase Order" + task1 = createTask(taskIdCounter++, "pending", "approval", "user001", "Review Purchase Order", 5); + // Task 2: unassigned, priority=10, type=approval, title="Approve Budget Report" + task2 = createTask(taskIdCounter++, "pending", "approval", null, "Approve Budget Report", 10); + // Task 3: assigned to user002, priority=1, type=leave, title="Submit Timesheet" + task3 = createTask(taskIdCounter++, "completed", "leave", "user002", "Submit Timesheet", 1); + // Task 4: assigned to admin-user, priority=8, type=leave, title="Review Leave Application" + task4 = createTask(taskIdCounter++, "pending", "leave", "admin-user", "Review Leave Application", 8); + + // Process instances + process1 = createProcess(processIdCounter++, "running", "approval", "user001"); + process2 = createProcess(processIdCounter++, "completed", "leave", "user002"); + process3 = createProcess(processIdCounter++, "running", "leave", "user001"); + + // Deployments + deployment1 = createDeployment(deploymentIdCounter++, "approval", "APPROVAL_V1", + "Purchase Approval Flow", "active", "normal"); + deployment2 = createDeployment(deploymentIdCounter++, "leave", "LEAVE_V1", + "Leave Application Flow", "active", "normal"); + deployment3 = createDeployment(deploymentIdCounter++, "approval", "APPROVAL_V2", + "Purchase Approval Flow V2", "disabled", "normal"); + } + + // ====================== TaskQuery: createdAfter / createdBefore ====================== + + @Test + public void testCreatedAfterBefore() { + Date beforeCreate = addMinutes(new Date(), -5); + Date afterCreate = addMinutes(new Date(), 5); + + List result = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .createdAfter(beforeCreate) + .createdBefore(afterCreate) + .list(); + + Assert.assertEquals("Should find all 4 tasks within time range", 4, result.size()); + } + + @Test + public void testCreatedAfterFuture() { + Date futureDate = addMinutes(new Date(), 60); + + List result = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .createdAfter(futureDate) + .list(); + + Assert.assertTrue("No tasks created in the future", result.isEmpty()); + } + + // ====================== TaskQuery: taskTitleLike ====================== + + @Test + public void testTaskTitleLike() { + List result = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .taskTitleLike("Review") + .list(); + + Assert.assertEquals("Should find 2 tasks with 'Review' in title", 2, result.size()); + } + + @Test + public void testTaskTitleLikeNoMatch() { + List result = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .taskTitleLike("nonexistent") + .list(); + + Assert.assertTrue("No tasks should match", result.isEmpty()); + } + + @Test + public void testTaskTitleLikeConditional() { + // condition=false, titleLike should be ignored + List result = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .taskTitleLike(false, "nonexistent") + .list(); + + Assert.assertEquals("Conditional=false should not filter", 4, result.size()); + } + + // ====================== TaskQuery: taskUnassigned ====================== + + @Test + public void testTaskUnassigned() { + List result = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .taskUnassigned() + .list(); + + Assert.assertEquals("Should find 1 unassigned task", 1, result.size()); + Assert.assertNull("Task should have no assignee", result.get(0).getClaimUserId()); + } + + @Test + public void testTaskUnassignedWithStatus() { + List result = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .taskUnassigned() + .taskStatus("pending") + .list(); + + Assert.assertEquals("Should find 1 unassigned pending task", 1, result.size()); + } + + // ====================== TaskQuery: processDefinitionTypeIn ====================== + + @Test + public void testProcessDefinitionTypeIn() { + List result = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .processDefinitionTypeIn(Arrays.asList("approval", "leave")) + .list(); + + Assert.assertEquals("Should find all 4 tasks", 4, result.size()); + } + + @Test + public void testProcessDefinitionTypeInSingle() { + List result = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .processDefinitionTypeIn("approval") + .list(); + + Assert.assertEquals("Should find 2 approval tasks", 2, result.size()); + } + + @Test + public void testProcessDefinitionTypeInNoMatch() { + List result = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .processDefinitionTypeIn("nonexistent") + .list(); + + Assert.assertTrue("No tasks should match", result.isEmpty()); + } + + // ====================== TaskQuery: taskAssigneeLike ====================== + + @Test + public void testTaskAssigneeLike() { + // "user" matches user001, user002, and admin-user (contains "user") + List result = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .taskAssigneeLike("user00") + .list(); + + Assert.assertEquals("Should find 2 tasks with 'user00' in assignee", 2, result.size()); + } + + @Test + public void testTaskAssigneeLikeAdmin() { + List result = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .taskAssigneeLike("admin") + .list(); + + Assert.assertEquals("Should find 1 task with 'admin' in assignee", 1, result.size()); + } + + // ====================== TaskQuery: taskMinPriority / taskMaxPriority ====================== + + @Test + public void testTaskMinPriority() { + List result = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .taskMinPriority(5) + .list(); + + Assert.assertEquals("Should find 3 tasks with priority >= 5", 3, result.size()); + } + + @Test + public void testTaskMaxPriority() { + List result = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .taskMaxPriority(5) + .list(); + + Assert.assertEquals("Should find 2 tasks with priority <= 5", 2, result.size()); + } + + @Test + public void testTaskPriorityRange() { + List result = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .taskMinPriority(5) + .taskMaxPriority(8) + .list(); + + Assert.assertEquals("Should find 2 tasks with priority 5-8", 2, result.size()); + } + + // ====================== TaskQuery: withoutTenantId ====================== + + @Test + public void testWithoutTenantId() { + // Query without tenant filter should return tasks from all tenants + List withTenant = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .processDefinitionType("approval") + .list(); + + List withoutTenant = smartEngine.createTaskQuery() + .withoutTenantId() + .processDefinitionType("approval") + .taskTitleLike("Review Purchase") + .list(); + + Assert.assertEquals("With tenant should find 2", 2, withTenant.size()); + // Without tenant may find more (from other test data), but should at least find ours + Assert.assertTrue("Without tenant should find at least 1", withoutTenant.size() >= 1); + } + + // ====================== TaskQuery: count with new filters ====================== + + @Test + public void testCountWithTitleLike() { + long count = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .taskTitleLike("Review") + .count(); + + Assert.assertEquals("Should count 2 tasks with 'Review' in title", 2L, count); + } + + @Test + public void testCountWithPriorityRange() { + long count = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .taskMinPriority(5) + .taskMaxPriority(8) + .count(); + + Assert.assertEquals("Should count 2 tasks in priority range 5-8", 2L, count); + } + + // ====================== TaskQuery: combined filters ====================== + + @Test + public void testCombinedFilters() { + List result = smartEngine.createTaskQuery() + .tenantId(TENANT_ID) + .processDefinitionTypeIn("leave") + .taskStatus("pending") + .taskMinPriority(5) + .list(); + + Assert.assertEquals("Should find 1 pending leave task with priority >= 5", 1, result.size()); + Assert.assertEquals("Should be the Review Leave Application task", + "Review Leave Application", result.get(0).getTitle()); + } + + // ====================== ProcessInstanceQuery: processDefinitionTypeIn ====================== + + @Test + public void testProcessDefinitionTypeInOnProcess() { + List result = smartEngine.createProcessQuery() + .tenantId(TENANT_ID) + .processDefinitionTypeIn(Arrays.asList("approval", "leave")) + .list(); + + Assert.assertEquals("Should find all 3 processes", 3, result.size()); + } + + @Test + public void testProcessDefinitionTypeInSingleOnProcess() { + List result = smartEngine.createProcessQuery() + .tenantId(TENANT_ID) + .processDefinitionTypeIn("approval") + .list(); + + Assert.assertEquals("Should find 1 approval process", 1, result.size()); + } + + // ====================== ProcessInstanceQuery: involvedUser ====================== + + @Test + public void testInvolvedUser() { + List result = smartEngine.createProcessQuery() + .tenantId(TENANT_ID) + .involvedUser("user001") + .list(); + + Assert.assertEquals("user001 started 2 processes", 2, result.size()); + } + + @Test + public void testInvolvedUserNoMatch() { + List result = smartEngine.createProcessQuery() + .tenantId(TENANT_ID) + .involvedUser("nonexistent") + .list(); + + Assert.assertTrue("No processes for nonexistent user", result.isEmpty()); + } + + // ====================== ProcessInstanceQuery: withoutTenantId ====================== + + @Test + public void testProcessQueryWithoutTenantId() { + List withTenant = smartEngine.createProcessQuery() + .tenantId(TENANT_ID) + .processDefinitionType("approval") + .list(); + + List withoutTenant = smartEngine.createProcessQuery() + .withoutTenantId() + .involvedUser("user001") + .processDefinitionType("approval") + .list(); + + Assert.assertEquals("With tenant should find 1 approval", 1, withTenant.size()); + Assert.assertTrue("Without tenant should find at least 1", withoutTenant.size() >= 1); + } + + // ====================== DeploymentQuery: basic filters ====================== + + @Test + public void testDeploymentQueryByType() { + List result = smartEngine.createDeploymentQuery() + .tenantId(TENANT_ID) + .processDefinitionType("approval") + .list(); + + Assert.assertEquals("Should find 2 approval deployments", 2, result.size()); + } + + @Test + public void testDeploymentQueryByCode() { + List result = smartEngine.createDeploymentQuery() + .tenantId(TENANT_ID) + .processDefinitionCode("LEAVE_V1") + .list(); + + Assert.assertEquals("Should find 1 leave deployment", 1, result.size()); + } + + @Test + public void testDeploymentQueryByNameLike() { + List result = smartEngine.createDeploymentQuery() + .tenantId(TENANT_ID) + .processDefinitionNameLike("Purchase") + .list(); + + Assert.assertEquals("Should find 2 deployments with 'Purchase' in name", 2, result.size()); + } + + @Test + public void testDeploymentQueryByNameLikeConditional() { + List result = smartEngine.createDeploymentQuery() + .tenantId(TENANT_ID) + .processDefinitionNameLike(false, "nonexistent") + .list(); + + Assert.assertEquals("Conditional=false should not filter", 3, result.size()); + } + + @Test + public void testDeploymentQueryByStatus() { + List result = smartEngine.createDeploymentQuery() + .tenantId(TENANT_ID) + .deploymentStatus("active") + .list(); + + Assert.assertEquals("Should find 2 active deployments", 2, result.size()); + } + + @Test + public void testDeploymentQueryByStatusConditional() { + List result = smartEngine.createDeploymentQuery() + .tenantId(TENANT_ID) + .deploymentStatus(false, "disabled") + .list(); + + Assert.assertEquals("Conditional=false should not filter", 3, result.size()); + } + + // ====================== DeploymentQuery: count ====================== + + @Test + public void testDeploymentQueryCount() { + long count = smartEngine.createDeploymentQuery() + .tenantId(TENANT_ID) + .processDefinitionType("approval") + .count(); + + Assert.assertEquals("Should count 2 approval deployments", 2L, count); + } + + // ====================== DeploymentQuery: pagination ====================== + + @Test + public void testDeploymentQueryPagination() { + List page1 = smartEngine.createDeploymentQuery() + .tenantId(TENANT_ID) + .listPage(0, 2); + + Assert.assertEquals("Page 1 should have 2 items", 2, page1.size()); + + List page2 = smartEngine.createDeploymentQuery() + .tenantId(TENANT_ID) + .listPage(2, 2); + + Assert.assertEquals("Page 2 should have 1 item", 1, page2.size()); + } + + // ====================== DeploymentQuery: ordering ====================== + + @Test + public void testDeploymentQueryOrderByCreateTime() { + List result = smartEngine.createDeploymentQuery() + .tenantId(TENANT_ID) + .orderByCreateTime().asc() + .list(); + + Assert.assertEquals("Should find 3 deployments", 3, result.size()); + } + + // ====================== DeploymentQuery: singleResult ====================== + + @Test + public void testDeploymentQuerySingleResult() { + DeploymentInstance result = smartEngine.createDeploymentQuery() + .tenantId(TENANT_ID) + .processDefinitionCode("LEAVE_V1") + .singleResult(); + + Assert.assertNotNull("Should find single deployment", result); + Assert.assertEquals("Should be LEAVE_V1", "LEAVE_V1", result.getProcessDefinitionCode()); + } + + @Test + public void testDeploymentQuerySingleResultNull() { + DeploymentInstance result = smartEngine.createDeploymentQuery() + .tenantId(TENANT_ID) + .processDefinitionCode("NONEXISTENT") + .singleResult(); + + Assert.assertNull("Should return null for no match", result); + } + + // ====================== DeploymentQuery: combined filters ====================== + + @Test + public void testDeploymentQueryCombinedFilters() { + List result = smartEngine.createDeploymentQuery() + .tenantId(TENANT_ID) + .processDefinitionType("approval") + .deploymentStatus("active") + .processDefinitionNameLike("Purchase") + .list(); + + Assert.assertEquals("Should find 1 active approval deployment", 1, result.size()); + Assert.assertEquals("Should be APPROVAL_V1", "APPROVAL_V1", + result.get(0).getProcessDefinitionCode()); + } + + // ====================== DeploymentQuery: withoutTenantId ====================== + + @Test + public void testDeploymentQueryWithoutTenantId() { + List withTenant = smartEngine.createDeploymentQuery() + .tenantId(TENANT_ID) + .processDefinitionCode("LEAVE_V1") + .list(); + + List withoutTenant = smartEngine.createDeploymentQuery() + .withoutTenantId() + .processDefinitionCode("LEAVE_V1") + .list(); + + Assert.assertEquals("With tenant should find 1", 1, withTenant.size()); + Assert.assertTrue("Without tenant should find at least 1", withoutTenant.size() >= 1); + } + + // ====================== DeploymentQuery: deploymentInstanceId ====================== + + @Test + public void testDeploymentQueryById() { + DeploymentInstance result = smartEngine.createDeploymentQuery() + .tenantId(TENANT_ID) + .deploymentInstanceId(String.valueOf(deployment1.getId())) + .singleResult(); + + Assert.assertNotNull("Should find deployment by ID", result); + Assert.assertEquals("APPROVAL_V1", result.getProcessDefinitionCode()); + } + + // ====================== Helper Methods ====================== + + private TaskInstanceEntity createTask(long id, String status, String processDefinitionType, + String claimUserId, String title, int priority) { + TaskInstanceEntity entity = new TaskInstanceEntity(); + entity.setId(id); + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + entity.setProcessInstanceId(processIdCounter); + entity.setExecutionInstanceId(id + 1000L); + entity.setActivityInstanceId(id + 2000L); + entity.setProcessDefinitionIdAndVersion("testProcess:1"); + entity.setProcessDefinitionActivityId("userTask1"); + entity.setProcessDefinitionType(processDefinitionType); + entity.setClaimUserId(claimUserId); + entity.setTitle(title); + entity.setPriority(priority); + entity.setStatus(status); + entity.setTenantId(TENANT_ID); + taskInstanceDAO.insert(entity); + return entity; + } + + private ProcessInstanceEntity createProcess(long id, String status, String processDefinitionType, + String startUserId) { + ProcessInstanceEntity entity = new ProcessInstanceEntity(); + entity.setId(id); + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + entity.setProcessDefinitionIdAndVersion("testProcess:1"); + entity.setProcessDefinitionType(processDefinitionType); + entity.setStartUserId(startUserId); + entity.setStatus(status); + entity.setTenantId(TENANT_ID); + processInstanceDAO.insert(entity); + return entity; + } + + private DeploymentInstanceEntity createDeployment(long id, String processDefinitionType, + String code, String name, String status, + String logicStatus) { + DeploymentInstanceEntity entity = new DeploymentInstanceEntity(); + entity.setId(id); + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + entity.setProcessDefinitionId("pd_" + id); + entity.setProcessDefinitionVersion("1.0"); + entity.setProcessDefinitionType(processDefinitionType); + entity.setProcessDefinitionCode(code); + entity.setProcessDefinitionName(name); + entity.setProcessDefinitionDesc("Test deployment " + name); + entity.setProcessDefinitionContent(""); + entity.setDeploymentUserId("deployer001"); + entity.setDeploymentStatus(status); + entity.setLogicStatus(logicStatus); + entity.setTenantId(TENANT_ID); + deploymentInstanceDAO.insert(entity); + return entity; + } + + private Date addMinutes(Date date, int minutes) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.add(Calendar.MINUTE, minutes); + return cal.getTime(); + } +} diff --git a/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/query/TaskCandidateQueryIntegrationTest.java b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/query/TaskCandidateQueryIntegrationTest.java new file mode 100644 index 000000000..07d82308f --- /dev/null +++ b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/query/TaskCandidateQueryIntegrationTest.java @@ -0,0 +1,495 @@ +package com.alibaba.smart.framework.engine.test.query; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.alibaba.smart.framework.engine.constant.RequestMapSpecialKeyConstant; +import com.alibaba.smart.framework.engine.constant.TaskInstanceConstant; +import com.alibaba.smart.framework.engine.model.assembly.ProcessDefinition; +import com.alibaba.smart.framework.engine.model.instance.ProcessInstance; +import com.alibaba.smart.framework.engine.model.instance.TaskInstance; +import com.alibaba.smart.framework.engine.service.param.query.PendingTaskQueryParam; +import com.alibaba.smart.framework.engine.service.param.query.TaskInstanceQueryByAssigneeParam; +import com.alibaba.smart.framework.engine.test.DatabaseBaseTestCase; +import com.alibaba.smart.framework.engine.test.process.helper.CustomExceptioinProcessor; +import com.alibaba.smart.framework.engine.test.process.helper.CustomVariablePersister; +import com.alibaba.smart.framework.engine.test.process.helper.DefaultMultiInstanceCounter; +import com.alibaba.smart.framework.engine.test.process.helper.DoNothingLockStrategy; +import com.alibaba.smart.framework.engine.test.process.helper.dispatcher.IdAndGroupTaskAssigneeDispatcher; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for TaskQuery candidate user/group query methods. + * + * Tests the following new TaskQuery methods: + * - taskCandidateUser(String userId) + * - taskCandidateGroup(String groupId) + * - taskCandidateGroupIn(List groupIds) + * - taskCandidateOrGroup(String userId, List groupIds) + * + * Uses IdAndGroupTaskAssigneeDispatcher which assigns: + * - Users: testuser1, testuser3, testuser5 + * - Groups: testgroup11, testgroup22 + * + * @author SmartEngine Team + */ +@ContextConfiguration("/spring/application-test.xml") +@RunWith(SpringJUnit4ClassRunner.class) +@Transactional +public class TaskCandidateQueryIntegrationTest extends DatabaseBaseTestCase { + + private static final String TENANT_ID = "candidate-test-tenant"; + + @Override + protected void initProcessConfiguration() { + super.initProcessConfiguration(); + processEngineConfiguration.setExceptionProcessor(new CustomExceptioinProcessor()); + processEngineConfiguration.setTaskAssigneeDispatcher(new IdAndGroupTaskAssigneeDispatcher()); + processEngineConfiguration.setMultiInstanceCounter(new DefaultMultiInstanceCounter()); + processEngineConfiguration.setVariablePersister(new CustomVariablePersister()); + processEngineConfiguration.setLockStrategy(new DoNothingLockStrategy()); + } + + // ============ taskCandidateUser Tests ============ + + @Test + public void testTaskCandidateUser_findsTaskByAssigneeUser() { + ProcessInstance processInstance = deployAndStartProcess(); + + // testuser1 is a candidate user assigned by IdAndGroupTaskAssigneeDispatcher + List tasks = smartEngine.createTaskQuery() + .taskCandidateUser("testuser1") + .tenantId(TENANT_ID) + .list(); + + Assert.assertNotNull("Tasks should not be null", tasks); + Assert.assertFalse("Should find tasks for candidate user testuser1", tasks.isEmpty()); + Assert.assertEquals("Should find 1 task", 1, tasks.size()); + Assert.assertEquals("Task should belong to the started process", + processInstance.getInstanceId(), tasks.get(0).getProcessInstanceId()); + } + + @Test + public void testTaskCandidateUser_noResultForNonCandidate() { + deployAndStartProcess(); + + // nonexistent user should not find any tasks + List tasks = smartEngine.createTaskQuery() + .taskCandidateUser("nonexistent-user") + .tenantId(TENANT_ID) + .list(); + + Assert.assertNotNull("Tasks should not be null", tasks); + Assert.assertTrue("Should not find tasks for non-candidate user", tasks.isEmpty()); + } + + @Test + public void testTaskCandidateUser_multipleProcesses() { + deployAndStartProcess(); + deployAndStartProcess(); + deployAndStartProcess(); + + // testuser3 is also a candidate user + List tasks = smartEngine.createTaskQuery() + .taskCandidateUser("testuser3") + .tenantId(TENANT_ID) + .list(); + + Assert.assertEquals("Should find 3 tasks for testuser3", 3, tasks.size()); + } + + // ============ taskCandidateGroup Tests ============ + + @Test + public void testTaskCandidateGroup_findsTaskByGroup() { + ProcessInstance processInstance = deployAndStartProcess(); + + // testgroup11 is a candidate group assigned by IdAndGroupTaskAssigneeDispatcher + List tasks = smartEngine.createTaskQuery() + .taskCandidateGroup("testgroup11") + .tenantId(TENANT_ID) + .list(); + + Assert.assertNotNull("Tasks should not be null", tasks); + Assert.assertFalse("Should find tasks for candidate group testgroup11", tasks.isEmpty()); + Assert.assertEquals("Should find 1 task", 1, tasks.size()); + } + + @Test + public void testTaskCandidateGroup_noResultForNonCandidateGroup() { + deployAndStartProcess(); + + List tasks = smartEngine.createTaskQuery() + .taskCandidateGroup("nonexistent-group") + .tenantId(TENANT_ID) + .list(); + + Assert.assertTrue("Should not find tasks for non-candidate group", tasks.isEmpty()); + } + + // ============ taskCandidateGroupIn Tests ============ + + @Test + public void testTaskCandidateGroupIn_findsByMultipleGroups() { + deployAndStartProcess(); + + // Both testgroup11 and testgroup22 are candidate groups + List tasks = smartEngine.createTaskQuery() + .taskCandidateGroupIn(Arrays.asList("testgroup11", "testgroup22")) + .tenantId(TENANT_ID) + .list(); + + Assert.assertNotNull("Tasks should not be null", tasks); + // The task should be found (both groups are assigned to the same task) + Assert.assertFalse("Should find tasks for candidate groups", tasks.isEmpty()); + } + + @Test + public void testTaskCandidateGroupIn_varargs() { + deployAndStartProcess(); + + // Varargs overload + List tasks = smartEngine.createTaskQuery() + .taskCandidateGroupIn("testgroup11", "testgroup22") + .tenantId(TENANT_ID) + .list(); + + Assert.assertFalse("Varargs overload should find tasks", tasks.isEmpty()); + } + + @Test + public void testTaskCandidateGroupIn_partialMatch() { + deployAndStartProcess(); + + // One valid group + one non-existent group + List tasks = smartEngine.createTaskQuery() + .taskCandidateGroupIn(Arrays.asList("testgroup11", "nonexistent-group")) + .tenantId(TENANT_ID) + .list(); + + Assert.assertFalse("Should find tasks with at least one matching group", tasks.isEmpty()); + } + + // ============ taskCandidateOrGroup Tests ============ + + @Test + public void testTaskCandidateOrGroup_combinedUserAndGroups() { + deployAndStartProcess(); + + // Combined: user OR groups + List tasks = smartEngine.createTaskQuery() + .taskCandidateOrGroup("testuser1", Arrays.asList("testgroup11", "testgroup22")) + .tenantId(TENANT_ID) + .list(); + + Assert.assertNotNull("Tasks should not be null", tasks); + Assert.assertFalse("Should find tasks with combined OR query", tasks.isEmpty()); + // Since all are assigned to the same task, should get 1 distinct result + Assert.assertEquals("Should find 1 distinct task", 1, tasks.size()); + } + + @Test + public void testTaskCandidateOrGroup_userMatchOnly() { + deployAndStartProcess(); + + // User matches, groups don't + List tasks = smartEngine.createTaskQuery() + .taskCandidateOrGroup("testuser1", Collections.singletonList("nonexistent-group")) + .tenantId(TENANT_ID) + .list(); + + Assert.assertFalse("Should find tasks even if only user matches", tasks.isEmpty()); + } + + @Test + public void testTaskCandidateOrGroup_groupMatchOnly() { + deployAndStartProcess(); + + // User doesn't match, group matches + List tasks = smartEngine.createTaskQuery() + .taskCandidateOrGroup("nonexistent-user", Collections.singletonList("testgroup11")) + .tenantId(TENANT_ID) + .list(); + + Assert.assertFalse("Should find tasks even if only group matches", tasks.isEmpty()); + } + + @Test + public void testTaskCandidateOrGroup_neitherMatch() { + deployAndStartProcess(); + + List tasks = smartEngine.createTaskQuery() + .taskCandidateOrGroup("nonexistent-user", Collections.singletonList("nonexistent-group")) + .tenantId(TENANT_ID) + .list(); + + Assert.assertTrue("Should not find tasks when neither user nor group matches", tasks.isEmpty()); + } + + // ============ count() with candidate filters ============ + + @Test + public void testCandidateUser_count() { + deployAndStartProcess(); + deployAndStartProcess(); + + long count = smartEngine.createTaskQuery() + .taskCandidateUser("testuser1") + .tenantId(TENANT_ID) + .count(); + + Assert.assertEquals("Should count 2 tasks for testuser1", 2, count); + } + + @Test + public void testCandidateGroup_count() { + deployAndStartProcess(); + deployAndStartProcess(); + deployAndStartProcess(); + + long count = smartEngine.createTaskQuery() + .taskCandidateGroup("testgroup22") + .tenantId(TENANT_ID) + .count(); + + Assert.assertEquals("Should count 3 tasks for testgroup22", 3, count); + } + + @Test + public void testCandidateOrGroup_count() { + deployAndStartProcess(); + deployAndStartProcess(); + + long count = smartEngine.createTaskQuery() + .taskCandidateOrGroup("testuser5", Arrays.asList("testgroup11")) + .tenantId(TENANT_ID) + .count(); + + Assert.assertEquals("Should count 2 tasks for combined query", 2, count); + } + + // ============ Combined with other filters ============ + + @Test + public void testCandidateUser_withTaskStatus() { + ProcessInstance processInstance = deployAndStartProcess(); + + // Query pending tasks only + List pendingTasks = smartEngine.createTaskQuery() + .taskCandidateUser("testuser1") + .taskStatus(TaskInstanceConstant.PENDING) + .tenantId(TENANT_ID) + .list(); + + Assert.assertFalse("Should find pending tasks", pendingTasks.isEmpty()); + + // Query completed tasks (none should exist) + List completedTasks = smartEngine.createTaskQuery() + .taskCandidateUser("testuser1") + .taskStatus(TaskInstanceConstant.COMPLETED) + .tenantId(TENANT_ID) + .list(); + + Assert.assertTrue("Should not find completed tasks", completedTasks.isEmpty()); + } + + @Test + public void testCandidateUser_withProcessInstanceId() { + ProcessInstance process1 = deployAndStartProcess(); + ProcessInstance process2 = deployAndStartProcess(); + + // Filter by specific process instance + List tasks = smartEngine.createTaskQuery() + .taskCandidateUser("testuser1") + .processInstanceId(process1.getInstanceId()) + .tenantId(TENANT_ID) + .list(); + + Assert.assertEquals("Should find 1 task for process1", 1, tasks.size()); + Assert.assertEquals("Task should belong to process1", + process1.getInstanceId(), tasks.get(0).getProcessInstanceId()); + } + + @Test + public void testCandidateUser_withPagination() { + // Start 5 processes + for (int i = 0; i < 5; i++) { + deployAndStartProcess(); + } + + // Page 1: first 2 tasks + List page1 = smartEngine.createTaskQuery() + .taskCandidateUser("testuser1") + .tenantId(TENANT_ID) + .listPage(0, 2); + + Assert.assertEquals("Page 1 should have 2 tasks", 2, page1.size()); + + // Page 2: next 2 tasks + List page2 = smartEngine.createTaskQuery() + .taskCandidateUser("testuser1") + .tenantId(TENANT_ID) + .listPage(2, 2); + + Assert.assertEquals("Page 2 should have 2 tasks", 2, page2.size()); + + // Page 3: last 1 task + List page3 = smartEngine.createTaskQuery() + .taskCandidateUser("testuser1") + .tenantId(TENANT_ID) + .listPage(4, 2); + + Assert.assertEquals("Page 3 should have 1 task", 1, page3.size()); + + // Verify total count + long total = smartEngine.createTaskQuery() + .taskCandidateUser("testuser1") + .tenantId(TENANT_ID) + .count(); + + Assert.assertEquals("Total should be 5", 5, total); + } + + // ============ Equivalence with old API ============ + + @Test + public void testCandidateQuery_equivalentToFindPendingTaskList() { + deployAndStartProcess(); + deployAndStartProcess(); + + // Old API + PendingTaskQueryParam pendingParam = new PendingTaskQueryParam(); + pendingParam.setAssigneeUserId("testuser1"); + pendingParam.setAssigneeGroupIdList(Arrays.asList("testgroup11", "testgroup22")); + pendingParam.setTenantId(TENANT_ID); + pendingParam.setPageOffset(0); + pendingParam.setPageSize(100); + + List oldApiResult = taskQueryService.findPendingTaskList(pendingParam); + + // New fluent API equivalent + List fluentApiResult = smartEngine.createTaskQuery() + .taskCandidateOrGroup("testuser1", Arrays.asList("testgroup11", "testgroup22")) + .taskStatus(TaskInstanceConstant.PENDING) + .tenantId(TENANT_ID) + .listPage(0, 100); + + Assert.assertEquals("Fluent API should return same number of results as old API", + oldApiResult.size(), fluentApiResult.size()); + + // Verify they contain the same task IDs + for (TaskInstance oldTask : oldApiResult) { + boolean found = false; + for (TaskInstance newTask : fluentApiResult) { + if (oldTask.getInstanceId().equals(newTask.getInstanceId())) { + found = true; + break; + } + } + Assert.assertTrue("Fluent API result should contain task " + oldTask.getInstanceId(), found); + } + } + + @Test + public void testCandidateQuery_equivalentToFindTaskListByAssignee() { + deployAndStartProcess(); + + // Old API + TaskInstanceQueryByAssigneeParam assigneeParam = new TaskInstanceQueryByAssigneeParam(); + assigneeParam.setAssigneeUserId("testuser5"); + assigneeParam.setAssigneeGroupIdList(Arrays.asList("testgroup11")); + assigneeParam.setStatus(TaskInstanceConstant.PENDING); + assigneeParam.setTenantId(TENANT_ID); + assigneeParam.setPageOffset(0); + assigneeParam.setPageSize(100); + + List oldApiResult = taskQueryService.findTaskListByAssignee(assigneeParam); + + // New fluent API equivalent + List fluentApiResult = smartEngine.createTaskQuery() + .taskCandidateOrGroup("testuser5", Arrays.asList("testgroup11")) + .taskStatus(TaskInstanceConstant.PENDING) + .tenantId(TENANT_ID) + .listPage(0, 100); + + Assert.assertEquals("Fluent API should return same count as old API", + oldApiResult.size(), fluentApiResult.size()); + } + + @Test + public void testCandidateQuery_countEquivalentToOldApi() { + deployAndStartProcess(); + deployAndStartProcess(); + + // Old API count + PendingTaskQueryParam pendingParam = new PendingTaskQueryParam(); + pendingParam.setAssigneeUserId("testuser3"); + pendingParam.setTenantId(TENANT_ID); + + Long oldCount = taskQueryService.countPendingTaskList(pendingParam); + + // New fluent API count + long newCount = smartEngine.createTaskQuery() + .taskCandidateUser("testuser3") + .taskStatus(TaskInstanceConstant.PENDING) + .tenantId(TENANT_ID) + .count(); + + Assert.assertEquals("Count should match between old and new API", + oldCount.longValue(), newCount); + } + + // ============ Edge cases ============ + + @Test + public void testCandidateQuery_emptyGroupList() { + deployAndStartProcess(); + + // Empty group list should still work (only user filter) + List tasks = smartEngine.createTaskQuery() + .taskCandidateOrGroup("testuser1", Collections.emptyList()) + .tenantId(TENANT_ID) + .list(); + + // With empty group list, the SQL should fallback to user-only matching + Assert.assertFalse("Should find tasks with user-only filter", tasks.isEmpty()); + } + + @Test + public void testCandidateQuery_withProcessDefinitionType() { + deployAndStartProcess(); + + // The test BPMN deploys without a specific type, so query with a non-matching type + List noMatch = smartEngine.createTaskQuery() + .taskCandidateUser("testuser1") + .tenantId(TENANT_ID) + .list(); + + Assert.assertFalse("Baseline: should find tasks", noMatch.isEmpty()); + } + + // ============ Helper Methods ============ + + private ProcessInstance deployAndStartProcess() { + ProcessDefinition processDefinition = repositoryCommandService + .deploy("user-task-id-and-group-test.bpmn20.xml").getFirstProcessDefinition(); + + Map variables = new HashMap(); + variables.put(RequestMapSpecialKeyConstant.TENANT_ID, TENANT_ID); + + ProcessInstance processInstance = processCommandService.start( + processDefinition.getId(), processDefinition.getVersion(), variables + ); + + return processInstance; + } +} diff --git a/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/service/ProcessCompleteTimeIntegrationTest.java b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/service/ProcessCompleteTimeIntegrationTest.java new file mode 100644 index 000000000..7a1fbdf91 --- /dev/null +++ b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/service/ProcessCompleteTimeIntegrationTest.java @@ -0,0 +1,292 @@ +package com.alibaba.smart.framework.engine.test.service; + +import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.persister.database.dao.BaseElementTest; +import com.alibaba.smart.framework.engine.persister.database.dao.ProcessInstanceDAO; +import com.alibaba.smart.framework.engine.persister.database.dao.TaskInstanceDAO; +import com.alibaba.smart.framework.engine.persister.database.entity.ProcessInstanceEntity; +import com.alibaba.smart.framework.engine.persister.database.entity.TaskInstanceEntity; +import com.alibaba.smart.framework.engine.service.param.query.ProcessInstanceQueryParam; +import com.alibaba.smart.framework.engine.service.param.query.TaskInstanceQueryParam; +import lombok.Setter; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +/** + * Process complete time and query filter integration test + * + * @author SmartEngine Team + */ +public class ProcessCompleteTimeIntegrationTest extends BaseElementTest { + + @Setter(onMethod = @__({@Autowired})) + private ProcessInstanceDAO processInstanceDAO; + + @Setter(onMethod = @__({@Autowired})) + private TaskInstanceDAO taskInstanceDAO; + + private static final String TENANT_ID = "-3"; + + // ID counters for unique IDs + private long processIdCounter = 6001L; + private long taskIdCounter = 7001L; + + @Test + public void testProcessCompleteTimeIsSet() { + // Create a completed process instance + ProcessInstanceEntity process = createProcessInstance("completed", processIdCounter++); + + // Set complete time + Date completeTime = new Date(); + process.setCompleteTime(completeTime); + + // Update process + processInstanceDAO.update(process); + + // Query and verify + ProcessInstanceEntity retrieved = processInstanceDAO.findOne(process.getId(), TENANT_ID); + Assert.assertNotNull("Process should exist", retrieved); + Assert.assertNotNull("Complete time should be set", retrieved.getCompleteTime()); + Assert.assertEquals("Status should be completed", "completed", retrieved.getStatus()); + } + + @Test + public void testQueryCompletedProcessByTimeRange() { + Calendar cal = Calendar.getInstance(); + + // Process1: completed 3 days ago + cal.add(Calendar.DAY_OF_MONTH, -3); + Date threeDaysAgo = cal.getTime(); + ProcessInstanceEntity process1 = createProcessInstance("completed", processIdCounter++); + process1.setCompleteTime(threeDaysAgo); + processInstanceDAO.update(process1); + + // Process2: completed 2 days ago + cal = Calendar.getInstance(); + cal.add(Calendar.DAY_OF_MONTH, -2); + Date twoDaysAgo = cal.getTime(); + ProcessInstanceEntity process2 = createProcessInstance("completed", processIdCounter++); + process2.setCompleteTime(twoDaysAgo); + processInstanceDAO.update(process2); + + // Process3: completed 1 day ago + cal = Calendar.getInstance(); + cal.add(Calendar.DAY_OF_MONTH, -1); + Date oneDayAgo = cal.getTime(); + ProcessInstanceEntity process3 = createProcessInstance("completed", processIdCounter++); + process3.setCompleteTime(oneDayAgo); + processInstanceDAO.update(process3); + + // Process4: running (no complete time) + ProcessInstanceEntity process4 = createProcessInstance("running", processIdCounter++); + + // Query: processes completed between 2.5 days ago and 0.5 days ago + ProcessInstanceQueryParam queryParam = new ProcessInstanceQueryParam(); + queryParam.setStatus("completed"); + queryParam.setTenantId(TENANT_ID); + + cal = Calendar.getInstance(); + cal.add(Calendar.DAY_OF_MONTH, -2); + cal.add(Calendar.HOUR, -12); + queryParam.setCompleteTimeStart(cal.getTime()); + + cal = Calendar.getInstance(); + cal.add(Calendar.HOUR, -12); + queryParam.setCompleteTimeEnd(cal.getTime()); + + List results = processInstanceDAO.find(queryParam); + + Assert.assertNotNull("Results should not be null", results); + Assert.assertTrue("Should have at least 2 results", results.size() >= 2); + + // Verify all returned processes are completed + for (ProcessInstanceEntity result : results) { + Assert.assertEquals("Status should be completed", "completed", result.getStatus()); + Assert.assertNotNull("Complete time should not be null", result.getCompleteTime()); + } + } + + @Test + public void testTaskCompleteTimeFiltering() { + Calendar cal = Calendar.getInstance(); + + // Task1: completed 2 days ago + cal.add(Calendar.DAY_OF_MONTH, -2); + Date twoDaysAgo = cal.getTime(); + TaskInstanceEntity task1 = createTaskInstance("completed", taskIdCounter++); + task1.setCompleteTime(twoDaysAgo); + taskInstanceDAO.update(task1); + + // Task2: completed 1 day ago + cal = Calendar.getInstance(); + cal.add(Calendar.DAY_OF_MONTH, -1); + Date oneDayAgo = cal.getTime(); + TaskInstanceEntity task2 = createTaskInstance("completed", taskIdCounter++); + task2.setCompleteTime(oneDayAgo); + taskInstanceDAO.update(task2); + + // Task3: running + TaskInstanceEntity task3 = createTaskInstance("running", taskIdCounter++); + + // Query: tasks completed within last 1.5 days + TaskInstanceQueryParam queryParam = new TaskInstanceQueryParam(); + queryParam.setStatus("completed"); + queryParam.setTenantId(TENANT_ID); + + cal = Calendar.getInstance(); + cal.add(Calendar.DAY_OF_MONTH, -1); + cal.add(Calendar.HOUR, -12); + queryParam.setCompleteTimeStart(cal.getTime()); + + queryParam.setCompleteTimeEnd(new Date()); + + List results = taskInstanceDAO.findTaskList(queryParam); + + Assert.assertNotNull("Results should not be null", results); + + // Verify returned tasks are completed and within time range + for (TaskInstanceEntity result : results) { + Assert.assertEquals("Status should be completed", "completed", result.getStatus()); + Assert.assertNotNull("Complete time should not be null", result.getCompleteTime()); + } + } + + @Test + public void testRunningProcessHasNoCompleteTime() { + ProcessInstanceEntity runningProcess = createProcessInstance("running", processIdCounter++); + + ProcessInstanceEntity retrieved = processInstanceDAO.findOne(runningProcess.getId(), TENANT_ID); + Assert.assertNotNull("Process should exist", retrieved); + Assert.assertEquals("Status should be running", "running", retrieved.getStatus()); + Assert.assertNull("Complete time should be null for running process", retrieved.getCompleteTime()); + } + + @Test + public void testCompleteTimeSetWhenProcessCompletes() { + // Simulate process from running to completed + ProcessInstanceEntity process = createProcessInstance("running", processIdCounter++); + Assert.assertNull("Initial complete time should be null", process.getCompleteTime()); + + // Process completes + process.setStatus("completed"); + process.setCompleteTime(new Date()); + processInstanceDAO.update(process); + + // Verify + ProcessInstanceEntity retrieved = processInstanceDAO.findOne(process.getId(), TENANT_ID); + Assert.assertEquals("Status should be completed", "completed", retrieved.getStatus()); + Assert.assertNotNull("Complete time should be set", retrieved.getCompleteTime()); + + // Verify complete time is recent (within last minute) + long timeDiff = new Date().getTime() - retrieved.getCompleteTime().getTime(); + Assert.assertTrue("Complete time should be recent", timeDiff < 60000); + } + + @Test + public void testQueryOnlyCompletedProcessesInTimeRange() { + Calendar cal = Calendar.getInstance(); + + // 1. Completed yesterday + cal.add(Calendar.DAY_OF_MONTH, -1); + ProcessInstanceEntity completed1 = createProcessInstance("completed", processIdCounter++); + completed1.setCompleteTime(cal.getTime()); + processInstanceDAO.update(completed1); + + // 2. Completed today + ProcessInstanceEntity completed2 = createProcessInstance("completed", processIdCounter++); + completed2.setCompleteTime(new Date()); + processInstanceDAO.update(completed2); + + // 3. Running (no complete time) + ProcessInstanceEntity running = createProcessInstance("running", processIdCounter++); + + // 4. Cancelled (has complete time) + ProcessInstanceEntity cancelled = createProcessInstance("cancelled", processIdCounter++); + cancelled.setCompleteTime(new Date()); + processInstanceDAO.update(cancelled); + + // Query: completed processes in last 2 days + ProcessInstanceQueryParam queryParam = new ProcessInstanceQueryParam(); + queryParam.setStatus("completed"); + queryParam.setTenantId(TENANT_ID); + + cal = Calendar.getInstance(); + cal.add(Calendar.DAY_OF_MONTH, -2); + queryParam.setCompleteTimeStart(cal.getTime()); + queryParam.setCompleteTimeEnd(new Date()); + + List results = processInstanceDAO.find(queryParam); + + Assert.assertNotNull("Results should not be null", results); + + // Verify: all results are completed status + for (ProcessInstanceEntity result : results) { + Assert.assertEquals("All results should have completed status", "completed", result.getStatus()); + Assert.assertNotNull("All results should have complete time", result.getCompleteTime()); + } + } + + @Test + public void testHistoricalDataWithNullCompleteTime() { + // Simulate historical data: completed but complete_time is null + ProcessInstanceEntity historicalProcess = createProcessInstance("completed", processIdCounter++); + // Don't set completeTime, keep it null + + ProcessInstanceEntity retrieved = processInstanceDAO.findOne(historicalProcess.getId(), TENANT_ID); + Assert.assertEquals("Status should be completed", "completed", retrieved.getStatus()); + + // Query should handle null values + ProcessInstanceQueryParam queryParam = new ProcessInstanceQueryParam(); + queryParam.setStatus("completed"); + queryParam.setTenantId(TENANT_ID); + + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DAY_OF_MONTH, -7); + queryParam.setCompleteTimeStart(cal.getTime()); + queryParam.setCompleteTimeEnd(new Date()); + + // Query should not crash even with null complete time records + List results = processInstanceDAO.find(queryParam); + Assert.assertNotNull("Results should not be null even with historical data", results); + } + + // Helper method: create test process instance + private ProcessInstanceEntity createProcessInstance(String status, long id) { + ProcessInstanceEntity entity = new ProcessInstanceEntity(); + entity.setId(id); + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + entity.setProcessDefinitionIdAndVersion("testProcess:1"); + entity.setProcessDefinitionType("bpmn20"); + entity.setStatus(status); + entity.setTitle("Test Process"); + entity.setTenantId(TENANT_ID); + + processInstanceDAO.insert(entity); + return entity; + } + + // Helper method: create test task instance + private TaskInstanceEntity createTaskInstance(String status, long id) { + TaskInstanceEntity entity = new TaskInstanceEntity(); + entity.setId(id); + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + entity.setProcessInstanceId(1000L); + entity.setExecutionInstanceId(2000L); + entity.setActivityInstanceId(3000L); + entity.setProcessDefinitionIdAndVersion("testProcess:1"); + entity.setProcessDefinitionActivityId("testTask"); + entity.setTitle("Test Task"); + entity.setStatus(status); + entity.setTenantId(TENANT_ID); + + taskInstanceDAO.insert(entity); + return entity; + } +} diff --git a/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/service/SupervisionNotificationFluentQueryIntegrationTest.java b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/service/SupervisionNotificationFluentQueryIntegrationTest.java new file mode 100644 index 000000000..92b5d41b5 --- /dev/null +++ b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/service/SupervisionNotificationFluentQueryIntegrationTest.java @@ -0,0 +1,934 @@ +package com.alibaba.smart.framework.engine.test.service; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.alibaba.smart.framework.engine.constant.NotificationConstant; +import com.alibaba.smart.framework.engine.constant.RequestMapSpecialKeyConstant; +import com.alibaba.smart.framework.engine.constant.SupervisionConstant; +import com.alibaba.smart.framework.engine.constant.TaskInstanceConstant; +import com.alibaba.smart.framework.engine.model.assembly.ProcessDefinition; +import com.alibaba.smart.framework.engine.model.instance.NotificationInstance; +import com.alibaba.smart.framework.engine.model.instance.ProcessInstance; +import com.alibaba.smart.framework.engine.model.instance.SupervisionInstance; +import com.alibaba.smart.framework.engine.model.instance.TaskInstance; +import com.alibaba.smart.framework.engine.service.command.NotificationCommandService; +import com.alibaba.smart.framework.engine.service.command.SupervisionCommandService; +import com.alibaba.smart.framework.engine.service.param.query.NotificationQueryParam; +import com.alibaba.smart.framework.engine.service.param.query.SupervisionQueryParam; +import com.alibaba.smart.framework.engine.service.query.NotificationQueryService; +import com.alibaba.smart.framework.engine.service.query.SupervisionQueryService; +import com.alibaba.smart.framework.engine.test.DatabaseBaseTestCase; +import com.alibaba.smart.framework.engine.test.process.helper.CustomExceptioinProcessor; +import com.alibaba.smart.framework.engine.test.process.helper.CustomVariablePersister; +import com.alibaba.smart.framework.engine.test.process.helper.DefaultMultiInstanceCounter; +import com.alibaba.smart.framework.engine.test.process.helper.DoNothingLockStrategy; +import com.alibaba.smart.framework.engine.test.process.helper.dispatcher.IdAndGroupTaskAssigneeDispatcher; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration test for Supervision, Notification services and Fluent Query API. + * + * This test covers: + * - SupervisionCommandService: create/close/batch supervision + * - SupervisionQueryService: query/count supervision records + * - NotificationCommandService: send/mark notifications + * - NotificationQueryService: query/count notifications + * - TaskQuery: fluent task querying + * - ProcessInstanceQuery: fluent process querying + * - SupervisionQuery: fluent supervision querying + * - NotificationQuery: fluent notification querying + * + * @author SmartEngine Team + */ +@ContextConfiguration("/spring/application-test.xml") +@RunWith(SpringJUnit4ClassRunner.class) +@Transactional +public class SupervisionNotificationFluentQueryIntegrationTest extends DatabaseBaseTestCase { + + private static final String TENANT_ID = "test-tenant-001"; + + // Services under test + private SupervisionCommandService supervisionCommandService; + private SupervisionQueryService supervisionQueryService; + private NotificationCommandService notificationCommandService; + private NotificationQueryService notificationQueryService; + + @Override + protected void initProcessConfiguration() { + super.initProcessConfiguration(); + processEngineConfiguration.setExceptionProcessor(new CustomExceptioinProcessor()); + processEngineConfiguration.setTaskAssigneeDispatcher(new IdAndGroupTaskAssigneeDispatcher()); + processEngineConfiguration.setMultiInstanceCounter(new DefaultMultiInstanceCounter()); + processEngineConfiguration.setVariablePersister(new CustomVariablePersister()); + processEngineConfiguration.setLockStrategy(new DoNothingLockStrategy()); + } + + @Override + public void setUp() { + super.setUp(); + // Get supervision and notification services from smartEngine + supervisionCommandService = smartEngine.getSupervisionCommandService(); + supervisionQueryService = smartEngine.getSupervisionQueryService(); + notificationCommandService = smartEngine.getNotificationCommandService(); + notificationQueryService = smartEngine.getNotificationQueryService(); + } + + // ============ SupervisionCommandService Tests ============ + + @Test + public void testCreateSupervision() { + // Deploy and start process + ProcessInstance processInstance = deployAndStartProcess(); + List tasks = taskQueryService.findAllPendingTaskList(processInstance.getInstanceId()); + Assert.assertFalse("Should have pending tasks", tasks.isEmpty()); + + TaskInstance task = tasks.get(0); + + // Create supervision with valid type: urge + SupervisionInstance supervision = supervisionCommandService.createSupervision( + task.getInstanceId(), + "supervisor001", + "Task is overdue, please handle urgently", + SupervisionConstant.SupervisionType.URGE, + TENANT_ID + ); + + Assert.assertNotNull("Supervision should be created", supervision); + Assert.assertNotNull("Supervision ID should not be null", supervision.getInstanceId()); + Assert.assertEquals("Supervisor should match", "supervisor001", supervision.getSupervisorUserId()); + Assert.assertEquals("Reason should match", "Task is overdue, please handle urgently", supervision.getSupervisionReason()); + Assert.assertEquals("Type should match", SupervisionConstant.SupervisionType.URGE, supervision.getSupervisionType()); + Assert.assertEquals("Status should be active", SupervisionConstant.SupervisionStatus.ACTIVE, supervision.getStatus()); + } + + @Test + public void testCloseSupervision() { + // Deploy and start process + ProcessInstance processInstance = deployAndStartProcess(); + List tasks = taskQueryService.findAllPendingTaskList(processInstance.getInstanceId()); + TaskInstance task = tasks.get(0); + + // Create supervision + SupervisionInstance supervision = supervisionCommandService.createSupervision( + task.getInstanceId(), + "supervisor002", + "Please expedite", + SupervisionConstant.SupervisionType.REMIND, + TENANT_ID + ); + + // Close supervision + supervisionCommandService.closeSupervision(supervision.getInstanceId(), TENANT_ID); + + // Verify closed + SupervisionInstance closedSupervision = supervisionQueryService.findOne( + supervision.getInstanceId(), TENANT_ID); + Assert.assertNotNull("Supervision should exist", closedSupervision); + Assert.assertEquals("Status should be closed", SupervisionConstant.SupervisionStatus.CLOSED, closedSupervision.getStatus()); + Assert.assertNotNull("Close time should be set", closedSupervision.getCloseTime()); + } + + @Test + public void testBatchCreateSupervision() { + // Deploy and start multiple processes + ProcessInstance process1 = deployAndStartProcess(); + ProcessInstance process2 = deployAndStartProcess(); + + List tasks1 = taskQueryService.findAllPendingTaskList(process1.getInstanceId()); + List tasks2 = taskQueryService.findAllPendingTaskList(process2.getInstanceId()); + + List taskIds = Arrays.asList( + tasks1.get(0).getInstanceId(), + tasks2.get(0).getInstanceId() + ); + + // Batch create supervision + List supervisions = supervisionCommandService.batchCreateSupervision( + taskIds, + "supervisor003", + "Batch supervision for overdue tasks", + SupervisionConstant.SupervisionType.TRACK, + TENANT_ID + ); + + Assert.assertNotNull("Supervisions should be created", supervisions); + Assert.assertEquals("Should have 2 supervisions", 2, supervisions.size()); + + for (SupervisionInstance supervision : supervisions) { + Assert.assertEquals("Supervisor should match", "supervisor003", supervision.getSupervisorUserId()); + Assert.assertEquals("Status should be active", SupervisionConstant.SupervisionStatus.ACTIVE, supervision.getStatus()); + } + } + + @Test + public void testAutoCloseSupervisionByTask() { + // Deploy and start process + ProcessInstance processInstance = deployAndStartProcess(); + List tasks = taskQueryService.findAllPendingTaskList(processInstance.getInstanceId()); + TaskInstance task = tasks.get(0); + + // Create supervision + SupervisionInstance supervision = supervisionCommandService.createSupervision( + task.getInstanceId(), + "supervisor004", + "Auto close test", + SupervisionConstant.SupervisionType.URGE, + TENANT_ID + ); + + // Verify active + Assert.assertEquals("Status should be active", SupervisionConstant.SupervisionStatus.ACTIVE, supervision.getStatus()); + + // Auto close by task completion + supervisionCommandService.autoCloseSupervisionByTask(task.getInstanceId(), TENANT_ID); + + // Verify closed + SupervisionInstance closedSupervision = supervisionQueryService.findOne( + supervision.getInstanceId(), TENANT_ID); + Assert.assertEquals("Status should be closed", SupervisionConstant.SupervisionStatus.CLOSED, closedSupervision.getStatus()); + } + + // ============ SupervisionQueryService Tests ============ + + @Test + public void testFindSupervisionList() { + // Create test data + ProcessInstance processInstance = deployAndStartProcess(); + List tasks = taskQueryService.findAllPendingTaskList(processInstance.getInstanceId()); + TaskInstance task = tasks.get(0); + + // Create multiple supervisions + supervisionCommandService.createSupervision(task.getInstanceId(), "supervisor005", "Reason 1", + SupervisionConstant.SupervisionType.URGE, TENANT_ID); + supervisionCommandService.createSupervision(task.getInstanceId(), "supervisor006", "Reason 2", + SupervisionConstant.SupervisionType.REMIND, TENANT_ID); + + // Query by param + SupervisionQueryParam param = new SupervisionQueryParam(); + param.setTenantId(TENANT_ID); + param.setTaskInstanceIdList(Collections.singletonList(task.getInstanceId())); + param.setPageOffset(0); + param.setPageSize(10); + + List supervisions = supervisionQueryService.findSupervisionList(param); + Assert.assertNotNull("Supervisions should not be null", supervisions); + Assert.assertEquals("Should have 2 supervisions", 2, supervisions.size()); + } + + @Test + public void testCountSupervision() { + ProcessInstance processInstance = deployAndStartProcess(); + List tasks = taskQueryService.findAllPendingTaskList(processInstance.getInstanceId()); + TaskInstance task = tasks.get(0); + + // Create supervisions + supervisionCommandService.createSupervision(task.getInstanceId(), "supervisor007", "Reason", + SupervisionConstant.SupervisionType.URGE, TENANT_ID); + supervisionCommandService.createSupervision(task.getInstanceId(), "supervisor008", "Reason", + SupervisionConstant.SupervisionType.URGE, TENANT_ID); + + // Count + SupervisionQueryParam param = new SupervisionQueryParam(); + param.setTenantId(TENANT_ID); + param.setTaskInstanceIdList(Collections.singletonList(task.getInstanceId())); + param.setStatus(SupervisionConstant.SupervisionStatus.ACTIVE); + + Long count = supervisionQueryService.countSupervision(param); + Assert.assertEquals("Should have 2 active supervisions", 2L, count.longValue()); + } + + @Test + public void testFindActiveSupervisionByTask() { + ProcessInstance processInstance = deployAndStartProcess(); + List tasks = taskQueryService.findAllPendingTaskList(processInstance.getInstanceId()); + TaskInstance task = tasks.get(0); + + // Create supervisions + SupervisionInstance supervision1 = supervisionCommandService.createSupervision( + task.getInstanceId(), "supervisor009", "Active", SupervisionConstant.SupervisionType.URGE, TENANT_ID); + SupervisionInstance supervision2 = supervisionCommandService.createSupervision( + task.getInstanceId(), "supervisor010", "Will be closed", SupervisionConstant.SupervisionType.REMIND, TENANT_ID); + + // Close one + supervisionCommandService.closeSupervision(supervision2.getInstanceId(), TENANT_ID); + + // Find active only + List activeSupervisions = supervisionQueryService.findActiveSupervisionByTask( + task.getInstanceId(), TENANT_ID); + + Assert.assertEquals("Should have 1 active supervision", 1, activeSupervisions.size()); + Assert.assertEquals("Should be the active one", supervision1.getInstanceId(), + activeSupervisions.get(0).getInstanceId()); + } + + // ============ NotificationCommandService Tests ============ + + @Test + public void testSendNotification() { + ProcessInstance processInstance = deployAndStartProcess(); + List tasks = taskQueryService.findAllPendingTaskList(processInstance.getInstanceId()); + TaskInstance task = tasks.get(0); + + // Send notification to multiple receivers + List receivers = Arrays.asList("user001", "user002", "user003"); + + List notifications = notificationCommandService.sendNotification( + processInstance.getInstanceId(), + task.getInstanceId(), + "sender001", + receivers, + "Task Notification", + "Please review the task", + TENANT_ID + ); + + Assert.assertNotNull("Notifications should be created", notifications); + Assert.assertEquals("Should have 3 notifications", 3, notifications.size()); + + for (NotificationInstance notification : notifications) { + Assert.assertEquals("Process instance should match", + processInstance.getInstanceId(), notification.getProcessInstanceId()); + Assert.assertEquals("Sender should match", "sender001", notification.getSenderUserId()); + Assert.assertEquals("Read status should be unread", NotificationConstant.ReadStatus.UNREAD, notification.getReadStatus()); + } + } + + @Test + public void testSendSingleNotification() { + ProcessInstance processInstance = deployAndStartProcess(); + + NotificationInstance notification = notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), + null, // no task + "sender002", + "receiver001", + "Process Started", + "A new approval process has been started", + NotificationConstant.NotificationType.INFORM, + TENANT_ID + ); + + Assert.assertNotNull("Notification should be created", notification); + Assert.assertEquals("Receiver should match", "receiver001", notification.getReceiverUserId()); + Assert.assertEquals("Type should match", NotificationConstant.NotificationType.INFORM, notification.getNotificationType()); + Assert.assertEquals("Title should match", "Process Started", notification.getTitle()); + } + + @Test + public void testMarkAsRead() { + ProcessInstance processInstance = deployAndStartProcess(); + + // Send notification + NotificationInstance notification = notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), + null, + "sender003", + "receiver002", + "Unread Notification", + "Content", + NotificationConstant.NotificationType.CC, + TENANT_ID + ); + + Assert.assertEquals("Should be unread initially", NotificationConstant.ReadStatus.UNREAD, notification.getReadStatus()); + + // Mark as read + notificationCommandService.markAsRead(notification.getInstanceId(), TENANT_ID); + + // Verify + NotificationInstance readNotification = notificationQueryService.findOne( + notification.getInstanceId(), TENANT_ID); + Assert.assertEquals("Should be read now", NotificationConstant.ReadStatus.READ, readNotification.getReadStatus()); + Assert.assertNotNull("Read time should be set", readNotification.getReadTime()); + } + + @Test + public void testBatchMarkAsRead() { + ProcessInstance processInstance = deployAndStartProcess(); + + // Send multiple notifications + NotificationInstance n1 = notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender004", "receiver003", "N1", "C1", + NotificationConstant.NotificationType.INFORM, TENANT_ID); + NotificationInstance n2 = notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender004", "receiver003", "N2", "C2", + NotificationConstant.NotificationType.INFORM, TENANT_ID); + NotificationInstance n3 = notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender004", "receiver003", "N3", "C3", + NotificationConstant.NotificationType.INFORM, TENANT_ID); + + // Batch mark as read + List notificationIds = Arrays.asList(n1.getInstanceId(), n2.getInstanceId(), n3.getInstanceId()); + notificationCommandService.batchMarkAsRead(notificationIds, TENANT_ID); + + // Verify all are read + for (String id : notificationIds) { + NotificationInstance notification = notificationQueryService.findOne(id, TENANT_ID); + Assert.assertEquals("Should be read", NotificationConstant.ReadStatus.READ, notification.getReadStatus()); + } + } + + // ============ NotificationQueryService Tests ============ + + @Test + public void testFindNotificationList() { + ProcessInstance processInstance = deployAndStartProcess(); + + // Send notifications + notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender005", "receiver004", "N1", "C1", + NotificationConstant.NotificationType.INFORM, TENANT_ID); + notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender005", "receiver004", "N2", "C2", + NotificationConstant.NotificationType.CC, TENANT_ID); + + // Query + NotificationQueryParam param = new NotificationQueryParam(); + param.setTenantId(TENANT_ID); + param.setReceiverUserId("receiver004"); + param.setPageOffset(0); + param.setPageSize(10); + + List notifications = notificationQueryService.findNotificationList(param); + Assert.assertNotNull("Notifications should not be null", notifications); + Assert.assertEquals("Should have 2 notifications", 2, notifications.size()); + } + + @Test + public void testCountUnreadNotifications() { + ProcessInstance processInstance = deployAndStartProcess(); + + // Send notifications + NotificationInstance n1 = notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender006", "receiver005", "N1", "C1", + NotificationConstant.NotificationType.INFORM, TENANT_ID); + notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender006", "receiver005", "N2", "C2", + NotificationConstant.NotificationType.INFORM, TENANT_ID); + notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender006", "receiver005", "N3", "C3", + NotificationConstant.NotificationType.INFORM, TENANT_ID); + + // Mark one as read + notificationCommandService.markAsRead(n1.getInstanceId(), TENANT_ID); + + // Count unread + Long unreadCount = notificationQueryService.countUnreadNotifications("receiver005", TENANT_ID); + Assert.assertEquals("Should have 2 unread notifications", 2L, unreadCount.longValue()); + } + + @Test + public void testFindByReceiver() { + ProcessInstance processInstance = deployAndStartProcess(); + + // Send to specific receiver + notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender007", "receiver006", "N1", "C1", + NotificationConstant.NotificationType.INFORM, TENANT_ID); + NotificationInstance n2 = notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender007", "receiver006", "N2", "C2", + NotificationConstant.NotificationType.INFORM, TENANT_ID); + + // Mark one as read + notificationCommandService.markAsRead(n2.getInstanceId(), TENANT_ID); + + // Find unread only + List unreadList = notificationQueryService.findByReceiver( + "receiver006", NotificationConstant.ReadStatus.UNREAD, TENANT_ID, 0, 10); + Assert.assertEquals("Should have 1 unread notification", 1, unreadList.size()); + + // Find all + List allList = notificationQueryService.findByReceiver( + "receiver006", null, TENANT_ID, 0, 10); + Assert.assertEquals("Should have 2 notifications total", 2, allList.size()); + } + + @Test + public void testFindBySender() { + ProcessInstance processInstance = deployAndStartProcess(); + + // Send from specific sender + notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender008", "receiver007", "N1", "C1", + NotificationConstant.NotificationType.INFORM, TENANT_ID); + notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender008", "receiver008", "N2", "C2", + NotificationConstant.NotificationType.INFORM, TENANT_ID); + + // Find by sender + List sentList = notificationQueryService.findBySender( + "sender008", TENANT_ID, 0, 10); + Assert.assertEquals("Should have 2 sent notifications", 2, sentList.size()); + } + + // ============ TaskQuery (Fluent API) Tests ============ + + @Test + public void testTaskQueryBasic() { + ProcessInstance processInstance = deployAndStartProcess(); + + // Use fluent API + List tasks = smartEngine.createTaskQuery() + .processInstanceId(processInstance.getInstanceId()) + .taskStatus(TaskInstanceConstant.PENDING) + .tenantId(TENANT_ID) + .list(); + + Assert.assertNotNull("Tasks should not be null", tasks); + Assert.assertFalse("Should have pending tasks", tasks.isEmpty()); + } + + @Test + public void testTaskQueryWithOrdering() { + // Start multiple processes to get more tasks + ProcessInstance process1 = deployAndStartProcess(); + ProcessInstance process2 = deployAndStartProcess(); + + // Use fluent API with ordering + List tasks = smartEngine.createTaskQuery() + .processInstanceIdIn(Arrays.asList(process1.getInstanceId(), process2.getInstanceId())) + .taskStatus(TaskInstanceConstant.PENDING) + .tenantId(TENANT_ID) + .orderByCreateTime().desc() + .list(); + + Assert.assertNotNull("Tasks should not be null", tasks); + Assert.assertTrue("Should have at least 2 tasks", tasks.size() >= 2); + } + + @Test + public void testTaskQueryWithPagination() { + // Start multiple processes + for (int i = 0; i < 5; i++) { + deployAndStartProcess(); + } + + // Query first page + List firstPage = smartEngine.createTaskQuery() + .taskStatus(TaskInstanceConstant.PENDING) + .tenantId(TENANT_ID) + .orderByCreateTime().desc() + .listPage(0, 2); + + Assert.assertEquals("First page should have 2 tasks", 2, firstPage.size()); + + // Query second page + List secondPage = smartEngine.createTaskQuery() + .taskStatus(TaskInstanceConstant.PENDING) + .tenantId(TENANT_ID) + .orderByCreateTime().desc() + .listPage(2, 2); + + Assert.assertEquals("Second page should have 2 tasks", 2, secondPage.size()); + } + + @Test + public void testTaskQueryCount() { + // Start processes + deployAndStartProcess(); + deployAndStartProcess(); + deployAndStartProcess(); + + // Count + long count = smartEngine.createTaskQuery() + .taskStatus(TaskInstanceConstant.PENDING) + .tenantId(TENANT_ID) + .count(); + + Assert.assertTrue("Should have at least 3 pending tasks", count >= 3); + } + + @Test + public void testTaskQuerySingleResult() { + ProcessInstance processInstance = deployAndStartProcess(); + List tasks = taskQueryService.findAllPendingTaskList(processInstance.getInstanceId()); + TaskInstance expectedTask = tasks.get(0); + + // Single result + TaskInstance task = smartEngine.createTaskQuery() + .taskInstanceId(expectedTask.getInstanceId()) + .tenantId(TENANT_ID) + .singleResult(); + + Assert.assertNotNull("Task should be found", task); + Assert.assertEquals("Task ID should match", expectedTask.getInstanceId(), task.getInstanceId()); + } + + // ============ ProcessInstanceQuery (Fluent API) Tests ============ + + @Test + public void testProcessQueryBasic() { + ProcessInstance createdProcess = deployAndStartProcess(); + + // Use fluent API + List processes = smartEngine.createProcessQuery() + .processInstanceId(createdProcess.getInstanceId()) + .tenantId(TENANT_ID) + .list(); + + Assert.assertNotNull("Processes should not be null", processes); + Assert.assertEquals("Should have 1 process", 1, processes.size()); + Assert.assertEquals("Process ID should match", createdProcess.getInstanceId(), + processes.get(0).getInstanceId()); + } + + @Test + public void testProcessQueryByStatus() { + // Start multiple processes + deployAndStartProcess(); + deployAndStartProcess(); + + // Query running processes + List runningProcesses = smartEngine.createProcessQuery() + .processStatus("running") + .tenantId(TENANT_ID) + .orderByStartTime().desc() + .list(); + + Assert.assertNotNull("Processes should not be null", runningProcesses); + Assert.assertTrue("Should have running processes", runningProcesses.size() >= 2); + + for (ProcessInstance process : runningProcesses) { + Assert.assertEquals("Status should be running", "running", process.getStatus().toString()); + } + } + + @Test + public void testProcessQueryWithPagination() { + // Start multiple processes + for (int i = 0; i < 5; i++) { + deployAndStartProcess(); + } + + // Query with pagination + List page1 = smartEngine.createProcessQuery() + .processStatus("running") + .tenantId(TENANT_ID) + .orderByStartTime().desc() + .listPage(0, 3); + + Assert.assertEquals("Should have 3 processes", 3, page1.size()); + + // Count total + long total = smartEngine.createProcessQuery() + .processStatus("running") + .tenantId(TENANT_ID) + .count(); + + Assert.assertTrue("Total should be at least 5", total >= 5); + } + + // ============ SupervisionQuery (Fluent API) Tests ============ + + @Test + public void testSupervisionQueryBasic() { + ProcessInstance processInstance = deployAndStartProcess(); + List tasks = taskQueryService.findAllPendingTaskList(processInstance.getInstanceId()); + TaskInstance task = tasks.get(0); + + // Create supervisions + supervisionCommandService.createSupervision(task.getInstanceId(), "supervisor011", "R1", + SupervisionConstant.SupervisionType.URGE, TENANT_ID); + supervisionCommandService.createSupervision(task.getInstanceId(), "supervisor012", "R2", + SupervisionConstant.SupervisionType.REMIND, TENANT_ID); + + // Use fluent API + List supervisions = smartEngine.createSupervisionQuery() + .taskInstanceId(task.getInstanceId()) + .supervisionStatus(SupervisionConstant.SupervisionStatus.ACTIVE) + .tenantId(TENANT_ID) + .list(); + + Assert.assertNotNull("Supervisions should not be null", supervisions); + Assert.assertEquals("Should have 2 supervisions", 2, supervisions.size()); + } + + @Test + public void testSupervisionQueryBySupervisor() { + ProcessInstance processInstance = deployAndStartProcess(); + List tasks = taskQueryService.findAllPendingTaskList(processInstance.getInstanceId()); + TaskInstance task = tasks.get(0); + + // Create supervisions by specific supervisor + supervisionCommandService.createSupervision(task.getInstanceId(), "supervisor013", "R1", + SupervisionConstant.SupervisionType.URGE, TENANT_ID); + supervisionCommandService.createSupervision(task.getInstanceId(), "supervisor013", "R2", + SupervisionConstant.SupervisionType.REMIND, TENANT_ID); + supervisionCommandService.createSupervision(task.getInstanceId(), "supervisor014", "R3", + SupervisionConstant.SupervisionType.URGE, TENANT_ID); + + // Query by supervisor + List supervisions = smartEngine.createSupervisionQuery() + .supervisorUserId("supervisor013") + .tenantId(TENANT_ID) + .orderByCreateTime().desc() + .list(); + + Assert.assertEquals("Should have 2 supervisions by supervisor013", 2, supervisions.size()); + } + + @Test + public void testSupervisionQueryWithOrdering() { + ProcessInstance processInstance = deployAndStartProcess(); + List tasks = taskQueryService.findAllPendingTaskList(processInstance.getInstanceId()); + TaskInstance task = tasks.get(0); + + // Create supervisions + supervisionCommandService.createSupervision(task.getInstanceId(), "supervisor015", "R1", + SupervisionConstant.SupervisionType.URGE, TENANT_ID); + SupervisionInstance s2 = supervisionCommandService.createSupervision( + task.getInstanceId(), "supervisor015", "R2", SupervisionConstant.SupervisionType.REMIND, TENANT_ID); + + // Close one + supervisionCommandService.closeSupervision(s2.getInstanceId(), TENANT_ID); + + // Query with ordering by close time + List closedSupervisions = smartEngine.createSupervisionQuery() + .supervisionStatus(SupervisionConstant.SupervisionStatus.CLOSED) + .tenantId(TENANT_ID) + .orderByCloseTime().desc() + .list(); + + Assert.assertFalse("Should have closed supervisions", closedSupervisions.isEmpty()); + } + + // ============ NotificationQuery (Fluent API) Tests ============ + + @Test + public void testNotificationQueryBasic() { + ProcessInstance processInstance = deployAndStartProcess(); + + // Send notifications + notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender009", "receiver009", "N1", "C1", + NotificationConstant.NotificationType.INFORM, TENANT_ID); + notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender009", "receiver009", "N2", "C2", + NotificationConstant.NotificationType.CC, TENANT_ID); + + // Use fluent API + List notifications = smartEngine.createNotificationQuery() + .receiverUserId("receiver009") + .tenantId(TENANT_ID) + .list(); + + Assert.assertNotNull("Notifications should not be null", notifications); + Assert.assertEquals("Should have 2 notifications", 2, notifications.size()); + } + + @Test + public void testNotificationQueryByReadStatus() { + ProcessInstance processInstance = deployAndStartProcess(); + + // Send notifications + NotificationInstance n1 = notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender010", "receiver010", "N1", "C1", + NotificationConstant.NotificationType.INFORM, TENANT_ID); + notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender010", "receiver010", "N2", "C2", + NotificationConstant.NotificationType.INFORM, TENANT_ID); + + // Mark one as read + notificationCommandService.markAsRead(n1.getInstanceId(), TENANT_ID); + + // Query unread only + List unreadNotifications = smartEngine.createNotificationQuery() + .receiverUserId("receiver010") + .readStatus(NotificationConstant.ReadStatus.UNREAD) + .tenantId(TENANT_ID) + .list(); + + Assert.assertEquals("Should have 1 unread notification", 1, unreadNotifications.size()); + } + + @Test + public void testNotificationQueryByType() { + ProcessInstance processInstance = deployAndStartProcess(); + + // Send different types + notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender011", "receiver011", "Inform", "C1", + NotificationConstant.NotificationType.INFORM, TENANT_ID); + notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender011", "receiver011", "CC", "C2", + NotificationConstant.NotificationType.CC, TENANT_ID); + + // Query by type + List informNotifications = smartEngine.createNotificationQuery() + .receiverUserId("receiver011") + .notificationType(NotificationConstant.NotificationType.INFORM) + .tenantId(TENANT_ID) + .list(); + + Assert.assertEquals("Should have 1 inform notification", 1, informNotifications.size()); + Assert.assertEquals("Type should be inform", NotificationConstant.NotificationType.INFORM, + informNotifications.get(0).getNotificationType()); + } + + @Test + public void testNotificationQueryWithPagination() { + ProcessInstance processInstance = deployAndStartProcess(); + + // Send many notifications + for (int i = 0; i < 10; i++) { + notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender012", "receiver012", + "N" + i, "Content " + i, NotificationConstant.NotificationType.INFORM, TENANT_ID); + } + + // Query with pagination + List page1 = smartEngine.createNotificationQuery() + .receiverUserId("receiver012") + .tenantId(TENANT_ID) + .orderByCreateTime().desc() + .listPage(0, 5); + + Assert.assertEquals("First page should have 5 notifications", 5, page1.size()); + + // Count total + long total = smartEngine.createNotificationQuery() + .receiverUserId("receiver012") + .tenantId(TENANT_ID) + .count(); + + Assert.assertEquals("Total should be 10", 10, total); + } + + @Test + public void testNotificationQuerySingleResult() { + ProcessInstance processInstance = deployAndStartProcess(); + + NotificationInstance sent = notificationCommandService.sendSingleNotification( + processInstance.getInstanceId(), null, "sender013", "receiver013", "Single", "C", + NotificationConstant.NotificationType.INFORM, TENANT_ID); + + // Query single result + NotificationInstance notification = smartEngine.createNotificationQuery() + .notificationId(sent.getInstanceId()) + .tenantId(TENANT_ID) + .singleResult(); + + Assert.assertNotNull("Notification should be found", notification); + Assert.assertEquals("ID should match", sent.getInstanceId(), notification.getInstanceId()); + } + + // ============ Combined Scenario Tests ============ + + @Test + public void testFullWorkflowWithSupervisionAndNotification() { + // 1. Deploy and start process + ProcessInstance processInstance = deployAndStartProcess(); + Assert.assertNotNull("Process should be started", processInstance); + + // 2. Get pending task + List tasks = smartEngine.createTaskQuery() + .processInstanceId(processInstance.getInstanceId()) + .taskStatus(TaskInstanceConstant.PENDING) + .tenantId(TENANT_ID) + .list(); + Assert.assertFalse("Should have pending tasks", tasks.isEmpty()); + TaskInstance task = tasks.get(0); + + // 3. Create supervision on the task + SupervisionInstance supervision = supervisionCommandService.createSupervision( + task.getInstanceId(), + "supervisor100", + "Please handle this task urgently", + SupervisionConstant.SupervisionType.URGE, + TENANT_ID + ); + Assert.assertNotNull("Supervision should be created", supervision); + + // 4. Send notification to task assignees + List notifications = notificationCommandService.sendNotification( + processInstance.getInstanceId(), + task.getInstanceId(), + "supervisor100", + Arrays.asList("user100", "user101"), + "Task Supervision Notice", + "Your task has been supervised, please handle urgently", + TENANT_ID + ); + Assert.assertEquals("Should send 2 notifications", 2, notifications.size()); + + // 5. Verify we can query all related data using fluent API + + // Query process + ProcessInstance queriedProcess = smartEngine.createProcessQuery() + .processInstanceId(processInstance.getInstanceId()) + .tenantId(TENANT_ID) + .singleResult(); + Assert.assertNotNull("Process should be found", queriedProcess); + + // Query task + TaskInstance queriedTask = smartEngine.createTaskQuery() + .taskInstanceId(task.getInstanceId()) + .tenantId(TENANT_ID) + .singleResult(); + Assert.assertNotNull("Task should be found", queriedTask); + + // Query supervision + List queriedSupervisions = smartEngine.createSupervisionQuery() + .taskInstanceId(task.getInstanceId()) + .supervisionStatus(SupervisionConstant.SupervisionStatus.ACTIVE) + .tenantId(TENANT_ID) + .list(); + Assert.assertEquals("Should have 1 active supervision", 1, queriedSupervisions.size()); + + // Query notifications for each receiver + for (String receiver : Arrays.asList("user100", "user101")) { + List receiverNotifications = smartEngine.createNotificationQuery() + .receiverUserId(receiver) + .processInstanceId(processInstance.getInstanceId()) + .tenantId(TENANT_ID) + .list(); + Assert.assertEquals("Each receiver should have 1 notification", 1, receiverNotifications.size()); + } + + // 6. Mark notifications as read + for (NotificationInstance notification : notifications) { + notificationCommandService.markAsRead(notification.getInstanceId(), TENANT_ID); + } + + // Verify unread count is 0 + Long unreadCount = notificationQueryService.countUnreadNotifications("user100", TENANT_ID); + Assert.assertEquals("Should have 0 unread notifications", 0L, unreadCount.longValue()); + + // 7. Close supervision + supervisionCommandService.closeSupervision(supervision.getInstanceId(), TENANT_ID); + + // Verify no active supervisions + List activeSupervisions = smartEngine.createSupervisionQuery() + .taskInstanceId(task.getInstanceId()) + .supervisionStatus(SupervisionConstant.SupervisionStatus.ACTIVE) + .tenantId(TENANT_ID) + .list(); + Assert.assertTrue("Should have no active supervisions", activeSupervisions.isEmpty()); + } + + // ============ Helper Methods ============ + + private ProcessInstance deployAndStartProcess() { + ProcessDefinition processDefinition = repositoryCommandService + .deploy("user-task-id-and-group-test.bpmn20.xml").getFirstProcessDefinition(); + + Map variables = new HashMap<>(); + variables.put(RequestMapSpecialKeyConstant.TENANT_ID, TENANT_ID); + + ProcessInstance processInstance = processCommandService.start( + processDefinition.getId(), processDefinition.getVersion(), variables + ); + + return processInstance; + } +} diff --git a/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/service/TaskOperationRecordIntegrationTest.java b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/service/TaskOperationRecordIntegrationTest.java new file mode 100644 index 000000000..58d5572af --- /dev/null +++ b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/service/TaskOperationRecordIntegrationTest.java @@ -0,0 +1,345 @@ +package com.alibaba.smart.framework.engine.test.service; + +import com.alibaba.smart.framework.engine.common.util.DateUtil; +import com.alibaba.smart.framework.engine.persister.database.dao.AssigneeOperationRecordDAO; +import com.alibaba.smart.framework.engine.persister.database.dao.BaseElementTest; +import com.alibaba.smart.framework.engine.persister.database.dao.RollbackRecordDAO; +import com.alibaba.smart.framework.engine.persister.database.dao.TaskTransferRecordDAO; +import com.alibaba.smart.framework.engine.persister.database.entity.AssigneeOperationRecordEntity; +import com.alibaba.smart.framework.engine.persister.database.entity.RollbackRecordEntity; +import com.alibaba.smart.framework.engine.persister.database.entity.TaskTransferRecordEntity; +import lombok.Setter; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +/** + * Task operation record integration test + * Tests transfer, rollback, add/remove assignee operations + * + * @author SmartEngine Team + */ +public class TaskOperationRecordIntegrationTest extends BaseElementTest { + + @Setter(onMethod = @__({@Autowired})) + private TaskTransferRecordDAO taskTransferRecordDAO; + + @Setter(onMethod = @__({@Autowired})) + private AssigneeOperationRecordDAO assigneeOperationRecordDAO; + + @Setter(onMethod = @__({@Autowired})) + private RollbackRecordDAO rollbackRecordDAO; + + private static final String TENANT_ID = "-3"; + + // ID counters for unique IDs + private long transferIdCounter = 8001L; + private long assigneeOpIdCounter = 8101L; + private long rollbackIdCounter = 8201L; + + @Test + public void testTaskTransferWithReason() { + String fromUserId = "user001"; + String toUserId = "user002"; + Long taskInstanceId = 100001L; + String reason = "Workload too heavy, need to transfer to colleague"; + + TaskTransferRecordEntity entity = new TaskTransferRecordEntity(); + entity.setId(transferIdCounter++); + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + entity.setTaskInstanceId(taskInstanceId); + entity.setFromUserId(fromUserId); + entity.setToUserId(toUserId); + entity.setTransferReason(reason); + entity.setTenantId(TENANT_ID); + + taskTransferRecordDAO.insert(entity); + Assert.assertNotNull("Transfer record should be created", entity.getId()); + + // Verify + List records = taskTransferRecordDAO.selectByTaskInstanceId(taskInstanceId, TENANT_ID); + Assert.assertFalse("Should have transfer records", records.isEmpty()); + Assert.assertEquals("Transfer reason should match", reason, records.get(0).getTransferReason()); + Assert.assertEquals("From user should match", fromUserId, records.get(0).getFromUserId()); + Assert.assertEquals("To user should match", toUserId, records.get(0).getToUserId()); + } + + @Test + public void testMultipleTransfers() { + Long taskInstanceId = 100002L; + + // First transfer: user001 -> user002 + TaskTransferRecordEntity transfer1 = new TaskTransferRecordEntity(); + transfer1.setId(transferIdCounter++); + transfer1.setGmtCreate(DateUtil.getCurrentDate()); + transfer1.setGmtModified(DateUtil.getCurrentDate()); + transfer1.setTaskInstanceId(taskInstanceId); + transfer1.setFromUserId("user001"); + transfer1.setToUserId("user002"); + transfer1.setTransferReason("First transfer"); + transfer1.setTenantId(TENANT_ID); + taskTransferRecordDAO.insert(transfer1); + + // Second transfer: user002 -> user003 + TaskTransferRecordEntity transfer2 = new TaskTransferRecordEntity(); + transfer2.setId(transferIdCounter++); + transfer2.setGmtCreate(DateUtil.getCurrentDate()); + transfer2.setGmtModified(DateUtil.getCurrentDate()); + transfer2.setTaskInstanceId(taskInstanceId); + transfer2.setFromUserId("user002"); + transfer2.setToUserId("user003"); + transfer2.setTransferReason("Second transfer"); + transfer2.setTenantId(TENANT_ID); + taskTransferRecordDAO.insert(transfer2); + + // Query transfer chain + List transferChain = taskTransferRecordDAO.selectByTaskInstanceId(taskInstanceId, TENANT_ID); + Assert.assertEquals("Should have 2 transfer records", 2, transferChain.size()); + } + + @Test + public void testAddAssigneeWithReason() { + Long taskInstanceId = 100003L; + String operatorUserId = "manager001"; + String targetUserId = "expert001"; + String reason = "Need expert review"; + + AssigneeOperationRecordEntity entity = new AssigneeOperationRecordEntity(); + entity.setId(assigneeOpIdCounter++); + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + entity.setTaskInstanceId(taskInstanceId); + entity.setOperationType("add_assignee"); + entity.setOperatorUserId(operatorUserId); + entity.setTargetUserId(targetUserId); + entity.setOperationReason(reason); + entity.setTenantId(TENANT_ID); + + assigneeOperationRecordDAO.insert(entity); + Assert.assertNotNull("Add assignee record should be created", entity.getId()); + + // Verify + List records = assigneeOperationRecordDAO.selectByTaskInstanceId(taskInstanceId, TENANT_ID); + Assert.assertFalse("Should have operation records", records.isEmpty()); + Assert.assertEquals("Operation type should be add_assignee", "add_assignee", records.get(0).getOperationType()); + Assert.assertEquals("Target user should match", targetUserId, records.get(0).getTargetUserId()); + } + + @Test + public void testRemoveAssigneeWithReason() { + Long taskInstanceId = 100004L; + String operatorUserId = "manager002"; + String targetUserId = "user005"; + String reason = "Employee has left"; + + AssigneeOperationRecordEntity entity = new AssigneeOperationRecordEntity(); + entity.setId(assigneeOpIdCounter++); + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + entity.setTaskInstanceId(taskInstanceId); + entity.setOperationType("remove_assignee"); + entity.setOperatorUserId(operatorUserId); + entity.setTargetUserId(targetUserId); + entity.setOperationReason(reason); + entity.setTenantId(TENANT_ID); + + assigneeOperationRecordDAO.insert(entity); + Assert.assertNotNull("Remove assignee record should be created", entity.getId()); + + // Verify + List records = assigneeOperationRecordDAO.selectByTaskInstanceId(taskInstanceId, TENANT_ID); + Assert.assertFalse("Should have operation records", records.isEmpty()); + Assert.assertEquals("Operation type should be remove_assignee", "remove_assignee", records.get(0).getOperationType()); + Assert.assertEquals("Operation reason should match", reason, records.get(0).getOperationReason()); + } + + @Test + public void testAddAndRemoveAssigneeSequence() { + Long taskInstanceId = 100005L; + + // Add expert1 + AssigneeOperationRecordEntity add1 = new AssigneeOperationRecordEntity(); + add1.setId(assigneeOpIdCounter++); + add1.setGmtCreate(DateUtil.getCurrentDate()); + add1.setGmtModified(DateUtil.getCurrentDate()); + add1.setTaskInstanceId(taskInstanceId); + add1.setOperationType("add_assignee"); + add1.setOperatorUserId("manager003"); + add1.setTargetUserId("expert002"); + add1.setOperationReason("Need finance expert review"); + add1.setTenantId(TENANT_ID); + assigneeOperationRecordDAO.insert(add1); + + // Add expert2 + AssigneeOperationRecordEntity add2 = new AssigneeOperationRecordEntity(); + add2.setId(assigneeOpIdCounter++); + add2.setGmtCreate(DateUtil.getCurrentDate()); + add2.setGmtModified(DateUtil.getCurrentDate()); + add2.setTaskInstanceId(taskInstanceId); + add2.setOperationType("add_assignee"); + add2.setOperatorUserId("manager003"); + add2.setTargetUserId("expert003"); + add2.setOperationReason("Need legal expert review"); + add2.setTenantId(TENANT_ID); + assigneeOperationRecordDAO.insert(add2); + + // Remove expert1 (review completed) + AssigneeOperationRecordEntity remove1 = new AssigneeOperationRecordEntity(); + remove1.setId(assigneeOpIdCounter++); + remove1.setGmtCreate(DateUtil.getCurrentDate()); + remove1.setGmtModified(DateUtil.getCurrentDate()); + remove1.setTaskInstanceId(taskInstanceId); + remove1.setOperationType("remove_assignee"); + remove1.setOperatorUserId("manager003"); + remove1.setTargetUserId("expert002"); + remove1.setOperationReason("Finance review completed"); + remove1.setTenantId(TENANT_ID); + assigneeOperationRecordDAO.insert(remove1); + + // Query all operation records + List records = assigneeOperationRecordDAO.selectByTaskInstanceId(taskInstanceId, TENANT_ID); + Assert.assertEquals("Should have 3 operation records", 3, records.size()); + + // Count operation types + long addCount = records.stream().filter(r -> "add_assignee".equals(r.getOperationType())).count(); + long removeCount = records.stream().filter(r -> "remove_assignee".equals(r.getOperationType())).count(); + Assert.assertEquals("Should have 2 add operations", 2, addCount); + Assert.assertEquals("Should have 1 remove operation", 1, removeCount); + } + + @Test + public void testRollbackWithReason() { + Long processInstanceId = 200001L; + Long taskInstanceId = 100006L; + String fromActivityId = "userTask2"; + String toActivityId = "userTask1"; + String operatorUserId = "approver001"; + String reason = "Found error in previous step, need to rollback"; + + RollbackRecordEntity entity = new RollbackRecordEntity(); + entity.setId(rollbackIdCounter++); + entity.setGmtCreate(DateUtil.getCurrentDate()); + entity.setGmtModified(DateUtil.getCurrentDate()); + entity.setProcessInstanceId(processInstanceId); + entity.setTaskInstanceId(taskInstanceId); + entity.setRollbackType("specific"); + entity.setFromActivityId(fromActivityId); + entity.setToActivityId(toActivityId); + entity.setOperatorUserId(operatorUserId); + entity.setRollbackReason(reason); + entity.setTenantId(TENANT_ID); + + rollbackRecordDAO.insert(entity); + Assert.assertNotNull("Rollback record should be created", entity.getId()); + + // Verify + List records = rollbackRecordDAO.selectByProcessInstanceId(processInstanceId, TENANT_ID); + Assert.assertFalse("Should have rollback records", records.isEmpty()); + Assert.assertEquals("Rollback reason should match", reason, records.get(0).getRollbackReason()); + Assert.assertEquals("From activity should match", fromActivityId, records.get(0).getFromActivityId()); + Assert.assertEquals("To activity should match", toActivityId, records.get(0).getToActivityId()); + Assert.assertEquals("Operator should match", operatorUserId, records.get(0).getOperatorUserId()); + } + + @Test + public void testMultipleRollbacks() { + Long processInstanceId = 200002L; + + // First rollback: task3 -> task2 + RollbackRecordEntity rollback1 = new RollbackRecordEntity(); + rollback1.setId(rollbackIdCounter++); + rollback1.setGmtCreate(DateUtil.getCurrentDate()); + rollback1.setGmtModified(DateUtil.getCurrentDate()); + rollback1.setProcessInstanceId(processInstanceId); + rollback1.setTaskInstanceId(100007L); + rollback1.setRollbackType("previous"); + rollback1.setFromActivityId("userTask3"); + rollback1.setToActivityId("userTask2"); + rollback1.setOperatorUserId("approver002"); + rollback1.setRollbackReason("First rollback: approval rejected"); + rollback1.setTenantId(TENANT_ID); + rollbackRecordDAO.insert(rollback1); + + // Second rollback: task2 -> task1 + RollbackRecordEntity rollback2 = new RollbackRecordEntity(); + rollback2.setId(rollbackIdCounter++); + rollback2.setGmtCreate(DateUtil.getCurrentDate()); + rollback2.setGmtModified(DateUtil.getCurrentDate()); + rollback2.setProcessInstanceId(processInstanceId); + rollback2.setTaskInstanceId(100008L); + rollback2.setRollbackType("specific"); + rollback2.setFromActivityId("userTask2"); + rollback2.setToActivityId("userTask1"); + rollback2.setOperatorUserId("approver002"); + rollback2.setRollbackReason("Second rollback: need to restart"); + rollback2.setTenantId(TENANT_ID); + rollbackRecordDAO.insert(rollback2); + + // Query rollback history + List rollbackHistory = rollbackRecordDAO.selectByProcessInstanceId(processInstanceId, TENANT_ID); + Assert.assertEquals("Should have 2 rollback records", 2, rollbackHistory.size()); + } + + @Test + public void testOperationRecordAuditTrail() { + Long taskInstanceId = 100009L; + Long processInstanceId = 200003L; + + // 1. Task transfer + TaskTransferRecordEntity transfer = new TaskTransferRecordEntity(); + transfer.setId(transferIdCounter++); + transfer.setGmtCreate(DateUtil.getCurrentDate()); + transfer.setGmtModified(DateUtil.getCurrentDate()); + transfer.setTaskInstanceId(taskInstanceId); + transfer.setFromUserId("user001"); + transfer.setToUserId("user002"); + transfer.setTransferReason("Transfer to professional"); + transfer.setTenantId(TENANT_ID); + taskTransferRecordDAO.insert(transfer); + + // 2. Add assignee + AssigneeOperationRecordEntity addAssignee = new AssigneeOperationRecordEntity(); + addAssignee.setId(assigneeOpIdCounter++); + addAssignee.setGmtCreate(DateUtil.getCurrentDate()); + addAssignee.setGmtModified(DateUtil.getCurrentDate()); + addAssignee.setTaskInstanceId(taskInstanceId); + addAssignee.setOperationType("add_assignee"); + addAssignee.setOperatorUserId("manager004"); + addAssignee.setTargetUserId("approver003"); + addAssignee.setOperationReason("Add department manager for approval"); + addAssignee.setTenantId(TENANT_ID); + assigneeOperationRecordDAO.insert(addAssignee); + + // 3. Rollback + RollbackRecordEntity rollback = new RollbackRecordEntity(); + rollback.setId(rollbackIdCounter++); + rollback.setGmtCreate(DateUtil.getCurrentDate()); + rollback.setGmtModified(DateUtil.getCurrentDate()); + rollback.setProcessInstanceId(processInstanceId); + rollback.setTaskInstanceId(taskInstanceId); + rollback.setRollbackType("previous"); + rollback.setFromActivityId("approvalTask"); + rollback.setToActivityId("fillFormTask"); + rollback.setOperatorUserId("approver003"); + rollback.setRollbackReason("Form incomplete"); + rollback.setTenantId(TENANT_ID); + rollbackRecordDAO.insert(rollback); + + // Verify all operations have records + List transfers = taskTransferRecordDAO.selectByTaskInstanceId(taskInstanceId, TENANT_ID); + List operations = assigneeOperationRecordDAO.selectByTaskInstanceId(taskInstanceId, TENANT_ID); + List rollbacks = rollbackRecordDAO.selectByProcessInstanceId(processInstanceId, TENANT_ID); + + Assert.assertEquals("Should have 1 transfer record", 1, transfers.size()); + Assert.assertEquals("Should have 1 assignee operation record", 1, operations.size()); + Assert.assertEquals("Should have 1 rollback record", 1, rollbacks.size()); + + // Verify audit trail completeness + Assert.assertNotNull("Transfer reason should be recorded", transfers.get(0).getTransferReason()); + Assert.assertNotNull("Operation reason should be recorded", operations.get(0).getOperationReason()); + Assert.assertNotNull("Rollback reason should be recorded", rollbacks.get(0).getRollbackReason()); + } +} diff --git a/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/sharding/ShardingModeE2ETest.java b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/sharding/ShardingModeE2ETest.java new file mode 100644 index 000000000..982cf9531 --- /dev/null +++ b/extension/storage/storage-mysql/src/test/java/com/alibaba/smart/framework/engine/test/sharding/ShardingModeE2ETest.java @@ -0,0 +1,317 @@ +package com.alibaba.smart.framework.engine.test.sharding; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.alibaba.smart.framework.engine.configuration.ConfigurationOption; +import com.alibaba.smart.framework.engine.constant.RequestMapSpecialKeyConstant; +import com.alibaba.smart.framework.engine.constant.TaskInstanceConstant; +import com.alibaba.smart.framework.engine.model.assembly.ProcessDefinition; +import com.alibaba.smart.framework.engine.model.instance.InstanceStatus; +import com.alibaba.smart.framework.engine.model.instance.ProcessInstance; +import com.alibaba.smart.framework.engine.model.instance.TaskInstance; +import com.alibaba.smart.framework.engine.persister.database.dao.UserTaskIndexDAO; +import com.alibaba.smart.framework.engine.persister.database.entity.UserTaskIndexEntity; +import com.alibaba.smart.framework.engine.service.param.query.PendingTaskQueryParam; +import com.alibaba.smart.framework.engine.service.param.query.TaskInstanceQueryByAssigneeParam; +import com.alibaba.smart.framework.engine.test.DatabaseBaseTestCase; +import com.alibaba.smart.framework.engine.test.process.helper.CustomExceptioinProcessor; +import com.alibaba.smart.framework.engine.test.process.helper.CustomVariablePersister; +import com.alibaba.smart.framework.engine.test.process.helper.DefaultMultiInstanceCounter; +import com.alibaba.smart.framework.engine.test.process.helper.DoNothingLockStrategy; +import com.alibaba.smart.framework.engine.test.process.helper.dispatcher.IdAndGroupTaskAssigneeDispatcher; + +import lombok.Setter; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + +/** + * End-to-end integration tests for sharding mode. + * + * Verifies that enabling ShardingModeEnabledOption causes the engine to: + * - Populate se_user_task_index when task assignees are created + * - Route findPendingTaskList / findTaskListByAssignee to the index table + * - Clean up index entries when tasks reach terminal status (completed/canceled) + * + * Uses IdAndGroupTaskAssigneeDispatcher which assigns per task: + * Users: testuser1, testuser3, testuser5 + * Groups: testgroup11, testgroup22 + */ +@ContextConfiguration("/spring/application-test.xml") +@RunWith(SpringJUnit4ClassRunner.class) +@Transactional +public class ShardingModeE2ETest extends DatabaseBaseTestCase { + + private static final String TENANT_ID = "sharding-e2e-test"; + + @Setter(onMethod = @__({@Autowired})) + UserTaskIndexDAO userTaskIndexDAO; + + @Override + protected void initProcessConfiguration() { + super.initProcessConfiguration(); + + // Enable sharding mode (add to existing OptionContainer to keep defaults) + processEngineConfiguration.getOptionContainer().put(ConfigurationOption.SHARDING_MODE_ENABLED_OPTION); + + processEngineConfiguration.setExceptionProcessor(new CustomExceptioinProcessor()); + processEngineConfiguration.setTaskAssigneeDispatcher(new IdAndGroupTaskAssigneeDispatcher()); + processEngineConfiguration.setMultiInstanceCounter(new DefaultMultiInstanceCounter()); + processEngineConfiguration.setVariablePersister(new CustomVariablePersister()); + processEngineConfiguration.setLockStrategy(new DoNothingLockStrategy()); + } + + // ============================================= + // Test: Process start populates index table + // ============================================= + + @Test + public void testStartProcess_indexTablePopulatedForAllAssignees() { + ProcessInstance processInstance = deployAndStartProcess(); + + // IdAndGroupTaskAssigneeDispatcher creates 5 assignees (3 user + 2 group) + // Each should have an index entry in se_user_task_index + + // Verify user assignees + assertIndexCount("testuser1", null, 1); + assertIndexCount("testuser3", null, 1); + assertIndexCount("testuser5", null, 1); + + // Verify group assignees + assertIndexCountByGroup("testgroup11", 1); + assertIndexCountByGroup("testgroup22", 1); + + // Verify index entry content + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(TENANT_ID); + param.setAssigneeUserId("testuser1"); + List entries = userTaskIndexDAO.findByAssignee(param); + + Assert.assertEquals(1, entries.size()); + UserTaskIndexEntity entry = entries.get(0); + Assert.assertEquals(Long.valueOf(processInstance.getInstanceId()), entry.getProcessInstanceId()); + Assert.assertEquals(TaskInstanceConstant.PENDING, entry.getTaskStatus()); + Assert.assertEquals(TENANT_ID, entry.getTenantId()); + } + + // ============================================= + // Test: findPendingTaskList routes to index table + // ============================================= + + @Test + public void testFindPendingTaskList_routesToIndexTable() { + ProcessInstance processInstance = deployAndStartProcess(); + + PendingTaskQueryParam param = new PendingTaskQueryParam(); + param.setAssigneeUserId("testuser1"); + param.setTenantId(TENANT_ID); + param.setPageOffset(0); + param.setPageSize(100); + + List tasks = taskQueryService.findPendingTaskList(param); + + Assert.assertEquals(1, tasks.size()); + Assert.assertEquals(processInstance.getInstanceId(), tasks.get(0).getProcessInstanceId()); + Assert.assertEquals(TaskInstanceConstant.PENDING, tasks.get(0).getStatus()); + } + + // ============================================= + // Test: findTaskListByAssignee with user + group + // ============================================= + + @Test + public void testFindTaskListByAssignee_userAndGroupCombined() { + deployAndStartProcess(); + + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(TENANT_ID); + param.setAssigneeUserId("testuser1"); + param.setAssigneeGroupIdList(Arrays.asList("testgroup11", "testgroup22")); + + List tasks = taskQueryService.findTaskListByAssignee(param); + + // Should find the task (user OR group match) + Assert.assertFalse("Should find tasks for user+group combined query", tasks.isEmpty()); + } + + @Test + public void testFindTaskListByAssignee_groupOnly() { + deployAndStartProcess(); + + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(TENANT_ID); + param.setAssigneeGroupIdList(Arrays.asList("testgroup11")); + + List tasks = taskQueryService.findTaskListByAssignee(param); + Assert.assertEquals(1, tasks.size()); + } + + // ============================================= + // Test: countPendingTaskList routes to index table + // ============================================= + + @Test + public void testCountPendingTaskList_routesToIndexTable() { + deployAndStartProcess(); + deployAndStartProcess(); + + PendingTaskQueryParam param = new PendingTaskQueryParam(); + param.setAssigneeUserId("testuser1"); + param.setTenantId(TENANT_ID); + + Long count = taskQueryService.countPendingTaskList(param); + Assert.assertEquals(Long.valueOf(2), count); + } + + // ============================================= + // Test: Complete task → index entries cleaned up + // ============================================= + + @Test + public void testCompleteTask_allIndexEntriesCleaned() { + ProcessInstance processInstance = deployAndStartProcess(); + + // Verify index entries exist + assertIndexCount("testuser1", null, 1); + assertIndexCount("testuser3", null, 1); + assertIndexCountByGroup("testgroup11", 1); + + // Get and complete the task + List pendingTasks = taskQueryService.findAllPendingTaskList( + processInstance.getInstanceId()); + Assert.assertEquals(1, pendingTasks.size()); + + Map request = new HashMap(); + request.put(RequestMapSpecialKeyConstant.TASK_INSTANCE_CLAIM_USER_ID, "testuser1"); + taskCommandService.complete(pendingTasks.get(0).getInstanceId(), request); + + // Verify ALL index entries for this task are cleaned (all 5 assignees) + assertIndexCount("testuser1", null, 0); + assertIndexCount("testuser3", null, 0); + assertIndexCount("testuser5", null, 0); + assertIndexCountByGroup("testgroup11", 0); + assertIndexCountByGroup("testgroup22", 0); + + // Verify process completed + ProcessInstance finalInstance = processQueryService.findById(processInstance.getInstanceId()); + Assert.assertEquals(InstanceStatus.completed, finalInstance.getStatus()); + } + + @Test + public void testCompleteTask_pendingQueryReturnsEmpty() { + ProcessInstance processInstance = deployAndStartProcess(); + + // Complete the task + List pendingTasks = taskQueryService.findAllPendingTaskList( + processInstance.getInstanceId()); + Map request = new HashMap(); + request.put(RequestMapSpecialKeyConstant.TASK_INSTANCE_CLAIM_USER_ID, "testuser1"); + taskCommandService.complete(pendingTasks.get(0).getInstanceId(), request); + + // findPendingTaskList should return empty (goes through index table) + PendingTaskQueryParam param = new PendingTaskQueryParam(); + param.setAssigneeUserId("testuser1"); + param.setTenantId(TENANT_ID); + param.setPageOffset(0); + param.setPageSize(100); + + List afterComplete = taskQueryService.findPendingTaskList(param); + Assert.assertTrue("No pending tasks after completion", afterComplete.isEmpty()); + } + + // ============================================= + // Test: Multiple processes, complete one + // ============================================= + + @Test + public void testMultipleProcesses_completeOne_onlyThatIndexRemoved() { + ProcessInstance process1 = deployAndStartProcess(); + ProcessInstance process2 = deployAndStartProcess(); + + // Both processes should have index entries + assertIndexCount("testuser1", null, 2); + + // Complete task in process1 + List tasks1 = taskQueryService.findAllPendingTaskList(process1.getInstanceId()); + Map request = new HashMap(); + request.put(RequestMapSpecialKeyConstant.TASK_INSTANCE_CLAIM_USER_ID, "testuser1"); + taskCommandService.complete(tasks1.get(0).getInstanceId(), request); + + // Only process2's index entries should remain + assertIndexCount("testuser1", null, 1); + + // Verify remaining entry belongs to process2 + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(TENANT_ID); + param.setAssigneeUserId("testuser1"); + List remaining = userTaskIndexDAO.findByAssignee(param); + Assert.assertEquals(Long.valueOf(process2.getInstanceId()), remaining.get(0).getProcessInstanceId()); + } + + // ============================================= + // Test: Index data consistency with main table + // ============================================= + + @Test + public void testIndexData_consistentWithMainTable() { + ProcessInstance processInstance = deployAndStartProcess(); + + // Get task from main table + List mainTasks = taskQueryService.findAllPendingTaskList( + processInstance.getInstanceId()); + Assert.assertEquals(1, mainTasks.size()); + TaskInstance mainTask = mainTasks.get(0); + + // Get from index table + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(TENANT_ID); + param.setAssigneeUserId("testuser1"); + List indexEntries = userTaskIndexDAO.findByAssignee(param); + Assert.assertEquals(1, indexEntries.size()); + UserTaskIndexEntity indexEntry = indexEntries.get(0); + + // Verify key fields match + Assert.assertEquals(Long.valueOf(mainTask.getInstanceId()), indexEntry.getTaskInstanceId()); + Assert.assertEquals(Long.valueOf(mainTask.getProcessInstanceId()), indexEntry.getProcessInstanceId()); + Assert.assertEquals(mainTask.getStatus(), indexEntry.getTaskStatus()); + } + + // ============================================= + // Helper methods + // ============================================= + + private ProcessInstance deployAndStartProcess() { + ProcessDefinition processDefinition = repositoryCommandService + .deploy("user-task-id-and-group-test.bpmn20.xml").getFirstProcessDefinition(); + + Map variables = new HashMap(); + variables.put(RequestMapSpecialKeyConstant.TENANT_ID, TENANT_ID); + + return processCommandService.start( + processDefinition.getId(), processDefinition.getVersion(), variables); + } + + private void assertIndexCount(String userId, List groupIds, int expected) { + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(TENANT_ID); + param.setAssigneeUserId(userId); + param.setAssigneeGroupIdList(groupIds); + Integer count = userTaskIndexDAO.countByAssignee(param); + Assert.assertEquals("Index count mismatch for user=" + userId, Integer.valueOf(expected), count); + } + + private void assertIndexCountByGroup(String groupId, int expected) { + TaskInstanceQueryByAssigneeParam param = new TaskInstanceQueryByAssigneeParam(); + param.setTenantId(TENANT_ID); + param.setAssigneeGroupIdList(Arrays.asList(groupId)); + Integer count = userTaskIndexDAO.countByAssignee(param); + Assert.assertEquals("Index count mismatch for group=" + groupId, Integer.valueOf(expected), count); + } +} diff --git a/extension/storage/storage-mysql/src/test/resources/event-based-gateway-test.bpmn20.xml b/extension/storage/storage-mysql/src/test/resources/event-based-gateway-test.bpmn20.xml new file mode 100644 index 000000000..9f96f3d43 --- /dev/null +++ b/extension/storage/storage-mysql/src/test/resources/event-based-gateway-test.bpmn20.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extension/storage/storage-mysql/src/test/resources/spring/application-test.xml b/extension/storage/storage-mysql/src/test/resources/spring/application-test.xml index 1f55d9135..1eb233a4e 100644 --- a/extension/storage/storage-mysql/src/test/resources/spring/application-test.xml +++ b/extension/storage/storage-mysql/src/test/resources/spring/application-test.xml @@ -21,6 +21,17 @@ + + + + + + + + + + + @@ -34,10 +45,10 @@ - - + + - + @@ -47,6 +58,9 @@ + + + com.alibaba.smart.framework smart-engine - + - 3.6.0-SNAPSHOT + 3.7.0-SNAPSHOT pom Smart Engine @@ -76,11 +76,14 @@ extension/storage/storage-common extension/storage/storage-custom extension/storage/storage-mysql + extension/storage/storage-dual extension/retry/retry-common extension/retry/retry-custom extension/retry/retry-mysql + extension/archive/archive-mysql + @@ -106,6 +109,10 @@ org.junit.jupiter junit-jupiter-engine + + org.junit.vintage + junit-vintage-engine + org.openjdk.jmh jmh-core @@ -159,6 +166,31 @@ mvel2 + + com.fasterxml.jackson.core + jackson-core + provided + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + + com.fasterxml.jackson.core + jackson-annotations + provided + + + + org.postgresql + postgresql + 42.7.4 + provided + + @@ -254,6 +286,12 @@ ${junit.jupiter.version} test + + org.junit.vintage + junit-vintage-engine + ${junit.jupiter.version} + test + @@ -363,6 +401,14 @@ test + + com.fasterxml.jackson + jackson-bom + 2.17.2 + pom + import + + @@ -457,12 +503,13 @@ org.apache.maven.plugins maven-source-plugin - 3.2.0 + 3.3.0 attach-sources + package - jar-no-fork + jar