feat: implement demo candidate 1.0.0-rc.1

This commit is contained in:
2026-09-03 21:48:12 +08:00
commit 043f8a823c
25 changed files with 9883 additions and 0 deletions

14
src/config/app.default.ts Normal file
View File

@@ -0,0 +1,14 @@
import type { MidwayConfig } from '@midwayjs/core';
import { defineSaasServiceHostConfig } from '@cool-midway/rpc/saas-host';
import { GeneratedSaasModule } from '../saas/generated';
const host = defineSaasServiceHostConfig({
generated: GeneratedSaasModule,
});
export default {
...host,
keys: 'saas-module-service-local-v1',
koa: { port: Number(process.env.PORT || 7101) },
asyncContextManager: { enable: true },
} as MidwayConfig;

12
src/configuration.ts Normal file
View File

@@ -0,0 +1,12 @@
import { Configuration } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import * as validate from '@midwayjs/validate';
import * as rpc from '@cool-midway/rpc';
import * as saasHost from '@cool-midway/rpc/saas-host';
import * as DefaultConfig from './config/app.default';
@Configuration({
imports: [koa, validate, rpc, saasHost],
importConfigs: [{ default: DefaultConfig }],
})
export class SaasModuleServiceConfiguration {}

90
src/controller/health.ts Normal file
View File

@@ -0,0 +1,90 @@
import { Config, Controller, Get, Inject } from '@midwayjs/core';
import type { Context } from '@midwayjs/koa';
import type {
RpcHostReadiness,
RpcHostReadinessSnapshot,
} from '@cool-midway/rpc/saas-host';
@Controller('/health')
export class SaasModuleHealthController {
@Inject()
ctx: Context;
@Config('cool.rpc.moduleCode')
moduleCode: string;
@Config('cool.rpc.serviceVersion')
serviceVersion: string;
@Config('cool.rpc.minSchemaVersion')
minSchemaVersion: number;
@Config('cool.rpc.maxSchemaVersion')
maxSchemaVersion: number;
@Get('/')
async health() {
const readiness = await this.resolveReadiness();
if (!readiness.ready) this.ctx.status = 503;
return {
...readiness,
moduleCode: this.moduleCode,
serviceVersion: this.serviceVersion,
schemaRange: [this.minSchemaVersion, this.maxSchemaVersion],
};
}
private async resolveReadiness(): Promise<RpcHostReadinessSnapshot> {
try {
const container = this.ctx?.requestContext;
if (!container) {
return this.unavailable('RPC_HOST_READINESS_PROVIDER_MISSING');
}
const readiness = await container.getAsync<RpcHostReadiness>(
'rpcHostReadiness'
);
if (!readiness) {
return this.unavailable('RPC_HOST_READINESS_PROVIDER_MISSING');
}
const snapshot = readiness?.snapshot?.();
return this.isReadinessSnapshot(snapshot)
? snapshot
: this.unavailable('RPC_HOST_READINESS_SNAPSHOT_INVALID');
} catch {
return this.unavailable('RPC_HOST_READINESS_UNAVAILABLE');
}
}
private isReadinessSnapshot(
snapshot: RpcHostReadinessSnapshot | undefined
): snapshot is RpcHostReadinessSnapshot {
const checkStates = ['pending', 'ok', 'error'];
return Boolean(
snapshot &&
typeof snapshot.ready === 'boolean' &&
['starting', 'ready', 'degraded', 'draining', 'stopped'].includes(
snapshot.status
) &&
snapshot.ready === (snapshot.status === 'ready') &&
Number.isFinite(snapshot.checkedAt) &&
snapshot.checks &&
checkStates.includes(snapshot.checks.bootstrap) &&
checkStates.includes(snapshot.checks.lease) &&
checkStates.includes(snapshot.checks.registry)
);
}
private unavailable(reason: string): RpcHostReadinessSnapshot {
return {
ready: false,
status: 'degraded',
reason,
checkedAt: Date.now(),
checks: {
bootstrap: 'error',
lease: 'error',
registry: 'error',
},
};
}
}

View File

@@ -0,0 +1 @@

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
export * from './models';
export * from './contracts';
export * from './saas-actions';
export * from './saas-events';
export * from './module';

View File

@@ -0,0 +1,78 @@
// Generated by cool saas-model generate. DO NOT EDIT.
import {
SaasReadModel,
SaasTableModel,
defineSaasReadModel,
defineSaasTableModel
} from '@cool-midway/module-runtime';
export interface GoodsRow extends Record<string, unknown> {
id: string;
title: string;
price: string;
stock: number;
status: number;
}
export class Goods extends SaasTableModel<GoodsRow> {
static readonly definition = defineSaasTableModel({
"kind": "table",
"moduleCode": "demo",
"revision": 1,
"schemaVersion": 1,
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
"tableCode": "goods",
"fields": [
{
"code": "id",
"type": "bigint",
"nullable": false,
"primary": true,
"readable": true,
"writable": false,
"filterable": true,
"sortable": true
},
{
"code": "title",
"type": "varchar",
"nullable": false,
"primary": false,
"readable": true,
"writable": true,
"filterable": true,
"sortable": true
},
{
"code": "price",
"type": "decimal",
"nullable": false,
"primary": false,
"readable": true,
"writable": true,
"filterable": true,
"sortable": true
},
{
"code": "stock",
"type": "int",
"nullable": false,
"primary": false,
"readable": true,
"writable": true,
"filterable": true,
"sortable": true
},
{
"code": "status",
"type": "int",
"nullable": false,
"primary": false,
"readable": true,
"writable": true,
"filterable": true,
"sortable": true
}
]
});
}

View File

@@ -0,0 +1,12 @@
// Generated by cool saas-model generate. DO NOT EDIT.
import { defineGeneratedSaasModule } from '@cool-midway/module-runtime';
export const GeneratedSaasModule = defineGeneratedSaasModule({
"formatVersion": 2,
"moduleCode": "demo",
"revision": 1,
"schemaVersion": 1,
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
"runtimeContractChecksum": "c1327810dd08dfd0cc1afeee99d7e9707fe14595272a3aa92ee13fc1d54aa0e5",
"implementationManifestChecksum": "d18964a3d40e3c9a65d83996ca3141a56043986c395f5d4cac57c60aa72f4236"
});

View File

@@ -0,0 +1,2 @@
// Generated by cool saas-model generate. DO NOT EDIT.
export const SaasActions = {} as const;

View File

@@ -0,0 +1,2 @@
// Generated by cool saas-model generate. DO NOT EDIT.
export const SaasEvents = {} as const;

View File

@@ -0,0 +1,957 @@
{
"manifestVersion": 1,
"moduleCode": "demo",
"revision": 1,
"schemaChecksum": null,
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
"actionManifestChecksum": null,
"runtimeContractChecksum": "c1327810dd08dfd0cc1afeee99d7e9707fe14595272a3aa92ee13fc1d54aa0e5",
"implementationManifestChecksum": "d18964a3d40e3c9a65d83996ca3141a56043986c395f5d4cac57c60aa72f4236",
"actions": [
"GoodsAdd",
"GoodsBatchInfo",
"GoodsDelete",
"GoodsInfo",
"GoodsList",
"GoodsPage",
"GoodsReleaseStock",
"GoodsReserveStock",
"GoodsUpdate"
],
"eventHandlers": [
"RuntimeSaasSagaCompensationCompleted",
"RuntimeSaasSagaCompensationFailed",
"RuntimeSaasSagaStepCompleted",
"RuntimeSaasSagaStepFailed"
],
"runtimeManagedEventHandlers": [
"RuntimeSaasSagaCompensationCompleted",
"RuntimeSaasSagaCompensationFailed",
"RuntimeSaasSagaStepCompleted",
"RuntimeSaasSagaStepFailed"
],
"runtimeManagedActions": [
"GoodsAdd",
"GoodsDelete",
"GoodsInfo",
"GoodsList",
"GoodsPage",
"GoodsUpdate"
],
"crudDescriptors": [
{
"exportName": "Goods",
"tableCode": "goods",
"operations": [
"add",
"delete",
"update",
"info",
"list",
"page"
],
"descriptorChecksum": "22211efee07c5c1aada51d508d9c053e0d7388ad6fc0da12c95a4d1de2e3ca53",
"actions": {
"add": "GoodsAdd",
"delete": "GoodsDelete",
"update": "GoodsUpdate",
"info": "GoodsInfo",
"list": "GoodsList",
"page": "GoodsPage"
}
}
],
"actionDescriptors": [
{
"name": "goods.add",
"origin": "table_crud",
"implementation": "runtime_managed",
"mutationKind": "insert",
"access": "write",
"idempotent": true,
"inputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"title": {
"type": "string",
"maxLength": 120
},
"price": {
"type": "string",
"pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?$"
},
"stock": {
"type": "integer"
},
"status": {
"type": "integer"
}
},
"required": [
"title",
"price"
]
},
"inputChecksum": "c2984884419729416e6cd81e4cddc6d82486b6dde2d9de0ca17533f69f55816a",
"outputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"pattern": "^-?[0-9]+$"
},
"title": {
"type": "string",
"maxLength": 120
},
"price": {
"type": "string",
"pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?$"
},
"stock": {
"type": "integer"
},
"status": {
"type": "integer"
}
},
"required": [
"id",
"title",
"price",
"stock",
"status"
]
},
"outputChecksum": "bb691ac7eaa19bb231f9da14a80848262965255fb37726ab155fb9804c9a55b1",
"callerPolicy": {
"kind": "permission",
"permission": "demo:goods:add"
},
"tables": [
{
"tableCode": "goods",
"fields": [
"id",
"price",
"status",
"stock",
"title"
],
"select": false
}
],
"readModels": [],
"writes": [
{
"tableCode": "goods",
"aggregateStream": "goods",
"sequenceCompleteness": "complete",
"idempotencyRequired": true,
"events": [
{
"eventType": "demo.goods.created",
"eventVersion": 1,
"eventContractChecksum": "c3ed1b37c636475284baae530668f10a5b22c07dbd91bc2ed5a12b9b52acbeee",
"operation": "upsert"
},
{
"eventType": "demo.goods.deleted",
"eventVersion": 1,
"eventContractChecksum": "61391ce0cf440dba05e89d48b2caccd1670af5fd072e63d4b16623bdc9f74db2",
"operation": "delete"
},
{
"eventType": "demo.goods.updated",
"eventVersion": 1,
"eventContractChecksum": "8206c1deed375f3ae71d4455109562c563edefceb75336d7b1a1b51a528c722c",
"operation": "upsert"
}
]
}
]
},
{
"name": "goods.batchInfo",
"origin": "business",
"implementation": "business",
"access": "read",
"idempotent": false,
"inputSchema": {
"type": "object",
"required": [
"ids"
],
"properties": {
"ids": {
"type": "array",
"items": {
"type": "integer",
"minimum": 1
},
"minItems": 1
}
},
"additionalProperties": false
},
"inputChecksum": "b2e0c5fd68adc75e2687c6a1ce01b8270240b42df6511efd6d922ea3242bc264",
"outputSchema": {
"type": "object",
"required": [
"list"
],
"properties": {
"list": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
}
},
"additionalProperties": false
},
"outputChecksum": "fe644424ef6a9e462b56edc9f70f48bb218783c69469469d4310de48589a0df5",
"callerPolicy": {
"kind": "module",
"allowedModules": [
"order"
]
},
"tables": [],
"readModels": [],
"writes": []
},
{
"name": "goods.delete",
"origin": "table_crud",
"implementation": "runtime_managed",
"mutationKind": "delete",
"access": "write",
"idempotent": true,
"inputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"pattern": "^-?[0-9]+$"
}
},
"required": [
"id"
]
},
"inputChecksum": "f7411322cb1460ead79eaa8267854f576188c583bfdc621d9cb6b5097991e865",
"outputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"affected": {
"type": "integer",
"minimum": 0
}
},
"required": [
"affected"
]
},
"outputChecksum": "5aa7994f78677fd557c2611cb26b6a1c4518cf4d6ccefa189a5c36af9f229716",
"callerPolicy": {
"kind": "permission",
"permission": "demo:goods:delete"
},
"tables": [
{
"tableCode": "goods",
"fields": [
"id"
],
"select": false
}
],
"readModels": [],
"writes": [
{
"tableCode": "goods",
"aggregateStream": "goods",
"sequenceCompleteness": "complete",
"idempotencyRequired": true,
"events": [
{
"eventType": "demo.goods.created",
"eventVersion": 1,
"eventContractChecksum": "c3ed1b37c636475284baae530668f10a5b22c07dbd91bc2ed5a12b9b52acbeee",
"operation": "upsert"
},
{
"eventType": "demo.goods.deleted",
"eventVersion": 1,
"eventContractChecksum": "61391ce0cf440dba05e89d48b2caccd1670af5fd072e63d4b16623bdc9f74db2",
"operation": "delete"
},
{
"eventType": "demo.goods.updated",
"eventVersion": 1,
"eventContractChecksum": "8206c1deed375f3ae71d4455109562c563edefceb75336d7b1a1b51a528c722c",
"operation": "upsert"
}
]
}
]
},
{
"name": "goods.info",
"origin": "table_crud",
"implementation": "runtime_managed",
"access": "read",
"idempotent": false,
"inputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"pattern": "^-?[0-9]+$"
}
},
"required": [
"id"
]
},
"inputChecksum": "f7411322cb1460ead79eaa8267854f576188c583bfdc621d9cb6b5097991e865",
"outputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"pattern": "^-?[0-9]+$"
},
"title": {
"type": "string",
"maxLength": 120
},
"price": {
"type": "string",
"pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?$"
},
"stock": {
"type": "integer"
},
"status": {
"type": "integer"
}
},
"required": [
"id",
"title",
"price",
"stock",
"status"
]
},
"outputChecksum": "bb691ac7eaa19bb231f9da14a80848262965255fb37726ab155fb9804c9a55b1",
"callerPolicy": {
"kind": "permission",
"permission": "demo:goods:info"
},
"tables": [
{
"tableCode": "goods",
"fields": [
"id",
"price",
"status",
"stock",
"title"
],
"select": true
}
],
"readModels": [],
"writes": []
},
{
"name": "goods.list",
"origin": "table_crud",
"implementation": "runtime_managed",
"access": "read",
"idempotent": false,
"inputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"where": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"pattern": "^-?[0-9]+$"
},
"title": {
"type": "string",
"maxLength": 120
},
"price": {
"type": "string",
"pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?$"
},
"stock": {
"type": "integer"
},
"status": {
"type": "integer"
}
},
"required": []
},
"order": {
"type": "object",
"additionalProperties": false,
"properties": {
"field": {
"type": "string",
"enum": [
"id",
"title",
"price",
"stock",
"status"
]
},
"direction": {
"type": "string",
"enum": [
"asc",
"desc"
]
}
},
"required": [
"field",
"direction"
]
},
"keyword": {
"type": "string",
"maxLength": 200
},
"take": {
"type": "integer",
"minimum": 1,
"maximum": 200
}
},
"required": []
},
"inputChecksum": "68074fd84863ef796fc3c99fc87d36c740f4a5bad7134a02b090f7cef6bde883",
"outputSchema": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"pattern": "^-?[0-9]+$"
},
"title": {
"type": "string",
"maxLength": 120
},
"price": {
"type": "string",
"pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?$"
},
"stock": {
"type": "integer"
},
"status": {
"type": "integer"
}
},
"required": [
"id",
"title",
"price",
"stock",
"status"
]
}
},
"outputChecksum": "2182ab26245d80a9a40216cce7cadbe8217ea12813f1cf536b7a59ba0a4ed4ae",
"callerPolicy": {
"kind": "permission",
"permission": "demo:goods:list"
},
"tables": [
{
"tableCode": "goods",
"fields": [
"id",
"price",
"status",
"stock",
"title"
],
"select": true
}
],
"readModels": [],
"writes": []
},
{
"name": "goods.page",
"origin": "table_crud",
"implementation": "runtime_managed",
"access": "read",
"idempotent": false,
"inputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"where": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"pattern": "^-?[0-9]+$"
},
"title": {
"type": "string",
"maxLength": 120
},
"price": {
"type": "string",
"pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?$"
},
"stock": {
"type": "integer"
},
"status": {
"type": "integer"
}
},
"required": []
},
"order": {
"type": "object",
"additionalProperties": false,
"properties": {
"field": {
"type": "string",
"enum": [
"id",
"title",
"price",
"stock",
"status"
]
},
"direction": {
"type": "string",
"enum": [
"asc",
"desc"
]
}
},
"required": [
"field",
"direction"
]
},
"keyword": {
"type": "string",
"maxLength": 200
},
"page": {
"type": "integer",
"minimum": 1
},
"size": {
"type": "integer",
"minimum": 1,
"maximum": 200
}
},
"required": []
},
"inputChecksum": "65eb38a87c2692fde220c350f971d8cc91c9ccc28e7bc41221092edaba042a4b",
"outputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"list": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"pattern": "^-?[0-9]+$"
},
"title": {
"type": "string",
"maxLength": 120
},
"price": {
"type": "string",
"pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?$"
},
"stock": {
"type": "integer"
},
"status": {
"type": "integer"
}
},
"required": [
"id",
"title",
"price",
"stock",
"status"
]
}
},
"pagination": {
"type": "object",
"additionalProperties": false,
"properties": {
"page": {
"type": "integer",
"minimum": 1
},
"size": {
"type": "integer",
"minimum": 1,
"maximum": 200
},
"total": {
"type": "integer",
"minimum": 0
}
},
"required": [
"page",
"size",
"total"
]
}
},
"required": [
"list",
"pagination"
]
},
"outputChecksum": "15ee81575d900276aaa2437afa5ee6f41321e5b00ab58706322e5d5c0809bf8d",
"callerPolicy": {
"kind": "permission",
"permission": "demo:goods:page"
},
"tables": [
{
"tableCode": "goods",
"fields": [
"id",
"price",
"status",
"stock",
"title"
],
"select": true
}
],
"readModels": [],
"writes": []
},
{
"name": "goods.releaseStock",
"origin": "business",
"implementation": "business",
"access": "write",
"idempotent": true,
"inputSchema": {
"type": "object",
"required": [
"goodsId",
"quantity",
"orderNo"
],
"properties": {
"goodsId": {
"type": "integer",
"minimum": 1
},
"orderNo": {
"type": "string",
"maxLength": 64,
"minLength": 1
},
"quantity": {
"type": "integer",
"minimum": 1
}
},
"additionalProperties": false
},
"inputChecksum": "c363d259b5108d069650a2554309eb3eb165d98723596595958e00acf8410abe",
"outputSchema": {
"type": "object",
"required": [
"goodsId",
"currentStock"
],
"properties": {
"goodsId": {
"type": "integer"
},
"currentStock": {
"type": "integer",
"minimum": 0
}
},
"additionalProperties": false
},
"outputChecksum": "a632b9f89518c3c17861454b74173a4880d5e8482639d3ff7f761aee4d061bd9",
"callerPolicy": {
"kind": "module",
"allowedModules": [
"order"
]
},
"tables": [],
"readModels": [],
"writes": [
{
"tableCode": "goods",
"aggregateStream": "goods",
"sequenceCompleteness": "complete",
"idempotencyRequired": true,
"events": [
{
"eventType": "demo.goods.stockReserved",
"eventVersion": 1,
"eventContractChecksum": "4d8a47d64f7d9fa43e0253228d05a2bb28db7efe3181e96d58dbff2163b9b752",
"operation": "upsert"
}
]
}
]
},
{
"name": "goods.reserveStock",
"origin": "business",
"implementation": "business",
"access": "write",
"idempotent": true,
"inputSchema": {
"type": "object",
"required": [
"goodsId",
"quantity"
],
"properties": {
"goodsId": {
"type": "integer",
"minimum": 1
},
"quantity": {
"type": "integer",
"minimum": 1
}
},
"additionalProperties": false
},
"inputChecksum": "6fbfa4d2324c4c0bdc621cbfd256353284e27495f59f47231b7d897b82a4e7b6",
"outputSchema": {
"type": "object",
"required": [
"goodsId",
"remainingStock"
],
"properties": {
"goodsId": {
"type": "integer"
},
"remainingStock": {
"type": "integer",
"minimum": 0
}
},
"additionalProperties": false
},
"outputChecksum": "545bd8b0bf6eb8670b21497236038187ee0f1df1544a318b16a399458fcb796e",
"callerPolicy": {
"kind": "module",
"allowedModules": [
"order"
]
},
"tables": [],
"readModels": [],
"writes": [
{
"tableCode": "goods",
"aggregateStream": "goods",
"sequenceCompleteness": "complete",
"idempotencyRequired": true,
"events": [
{
"eventType": "demo.goods.stockReserved",
"eventVersion": 1,
"eventContractChecksum": "4d8a47d64f7d9fa43e0253228d05a2bb28db7efe3181e96d58dbff2163b9b752",
"operation": "upsert"
}
]
}
]
},
{
"name": "goods.update",
"origin": "table_crud",
"implementation": "runtime_managed",
"mutationKind": "update",
"access": "write",
"idempotent": true,
"inputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"pattern": "^-?[0-9]+$"
},
"title": {
"type": "string",
"maxLength": 120
},
"price": {
"type": "string",
"pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?$"
},
"stock": {
"type": "integer"
},
"status": {
"type": "integer"
}
},
"required": [
"id"
],
"minProperties": 2
},
"inputChecksum": "ced49f501c83dd1dce9725c913a1761566828004c851779a5db85ddd2bb9123c",
"outputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"affected": {
"type": "integer",
"minimum": 0
}
},
"required": [
"affected"
]
},
"outputChecksum": "5aa7994f78677fd557c2611cb26b6a1c4518cf4d6ccefa189a5c36af9f229716",
"callerPolicy": {
"kind": "permission",
"permission": "demo:goods:update"
},
"tables": [
{
"tableCode": "goods",
"fields": [
"id",
"price",
"status",
"stock",
"title"
],
"select": false
}
],
"readModels": [],
"writes": [
{
"tableCode": "goods",
"aggregateStream": "goods",
"sequenceCompleteness": "complete",
"idempotencyRequired": true,
"events": [
{
"eventType": "demo.goods.created",
"eventVersion": 1,
"eventContractChecksum": "c3ed1b37c636475284baae530668f10a5b22c07dbd91bc2ed5a12b9b52acbeee",
"operation": "upsert"
},
{
"eventType": "demo.goods.deleted",
"eventVersion": 1,
"eventContractChecksum": "61391ce0cf440dba05e89d48b2caccd1670af5fd072e63d4b16623bdc9f74db2",
"operation": "delete"
},
{
"eventType": "demo.goods.updated",
"eventVersion": 1,
"eventContractChecksum": "8206c1deed375f3ae71d4455109562c563edefceb75336d7b1a1b51a528c722c",
"operation": "upsert"
}
]
}
]
}
],
"eventHandlerDescriptors": [
{
"eventType": "saas.saga.compensation.completed",
"eventVersion": 1,
"eventContractChecksum": "2919921c79a7795d6408b202d74aa0fb4b12ec56e1cec7de65eb95bbeeef2c5e",
"tables": [
"__saas_rpc_saga",
"__saas_rpc_saga_step"
],
"maxAttempts": 10
},
{
"eventType": "saas.saga.compensation.failed",
"eventVersion": 1,
"eventContractChecksum": "b7c7313c221e395c42e45e9480205a673108bdc395430bec892a9dafa39807f3",
"tables": [
"__saas_rpc_saga",
"__saas_rpc_saga_step"
],
"maxAttempts": 10
},
{
"eventType": "saas.saga.step.completed",
"eventVersion": 1,
"eventContractChecksum": "9d3acfdac4e5713672063f7e9f9b5b0b09487c59238d89dbe8d7145c6a09f1bd",
"tables": [
"__saas_rpc_saga",
"__saas_rpc_saga_step"
],
"maxAttempts": 10
},
{
"eventType": "saas.saga.step.failed",
"eventVersion": 1,
"eventContractChecksum": "8dd17e8104b78fa3ece7375402782dee38e121a87c91185658633fabaac8eba0",
"tables": [
"__saas_rpc_saga",
"__saas_rpc_saga_step"
],
"maxAttempts": 10
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
export * from './demo';

1
src/service/.gitkeep Normal file
View File

@@ -0,0 +1 @@

118
src/service/goods.ts Normal file
View File

@@ -0,0 +1,118 @@
import { Inject, Provide } from '@midwayjs/core';
import {
InjectSaasCrud,
SaasAction,
SaasActionContext,
SaasBusinessReject,
SaasCrudCaller,
SaasCrudService,
SaasCrudServiceBase,
} from '@cool-midway/module-runtime';
import {
Actions,
Crud,
Events,
type GoodsBatchInfoInput,
type GoodsBatchInfoOutput,
type GoodsInfoOutput,
type GoodsReleaseStockInput,
type GoodsReleaseStockOutput,
type GoodsReserveStockInput,
type GoodsReserveStockOutput,
} from '../saas/generated';
@Provide()
@SaasCrudService(Crud.Goods)
export class GoodsService extends SaasCrudServiceBase<typeof Crud.Goods> {
@InjectSaasCrud(Crud.Goods)
private readonly goods!: SaasCrudCaller<typeof Crud.Goods>;
@Inject()
private readonly saasActionContext!: SaasActionContext;
@SaasAction(Actions.GoodsBatchInfo)
async batchInfo(
input: GoodsBatchInfoInput
): Promise<GoodsBatchInfoOutput> {
const list: GoodsInfoOutput[] = [];
for (const id of input.ids) {
const goods = await this.findGoods(id);
if (goods) list.push(goods);
}
return { list };
}
@SaasAction(Actions.GoodsReserveStock)
async reserveStock(
input: GoodsReserveStockInput
): Promise<GoodsReserveStockOutput> {
const goods = await this.requireGoods(input.goodsId);
if (goods.status !== 1) {
throw new SaasBusinessReject(
'GOODS_UNAVAILABLE',
`商品 ${input.goodsId} 当前不可售`
);
}
if (goods.stock < input.quantity) {
throw new SaasBusinessReject(
'INSUFFICIENT_STOCK',
`商品 ${input.goodsId} 库存不足`,
{ available: goods.stock, requested: input.quantity }
);
}
const remainingStock = goods.stock - input.quantity;
this.saasActionContext.raise(Events.DemoGoodsStockReserved, {
goodsId: input.goodsId,
quantity: input.quantity,
remainingStock,
});
await this.updateStock(input.goodsId, remainingStock);
return { goodsId: input.goodsId, remainingStock };
}
@SaasAction(Actions.GoodsReleaseStock)
async releaseStock(
input: GoodsReleaseStockInput
): Promise<GoodsReleaseStockOutput> {
const goods = await this.requireGoods(input.goodsId);
const currentStock = goods.stock + input.quantity;
this.saasActionContext.raise(Events.DemoGoodsStockReserved, {
goodsId: input.goodsId,
quantity: input.quantity,
remainingStock: currentStock,
});
await this.updateStock(input.goodsId, currentStock);
return { goodsId: input.goodsId, currentStock };
}
private async findGoods(goodsId: number): Promise<GoodsInfoOutput | null> {
const rows = await this.goods.list({
where: { id: String(goodsId) },
take: 1,
});
return rows[0] || null;
}
private async requireGoods(goodsId: number): Promise<GoodsInfoOutput> {
const goods = await this.findGoods(goodsId);
if (!goods) {
throw new SaasBusinessReject(
'GOODS_NOT_FOUND',
`商品 ${goodsId} 不存在`
);
}
return goods;
}
private async updateStock(goodsId: number, stock: number): Promise<void> {
const result = await this.goods.update({ id: String(goodsId), stock });
if (result.affected !== 1) {
throw new SaasBusinessReject(
'GOODS_STOCK_UPDATE_FAILED',
`商品 ${goodsId} 库存更新失败`
);
}
}
}