优化
This commit is contained in:
@@ -187,14 +187,14 @@ public class FinanceService {
|
||||
for (var entry : byBill.entrySet()) {
|
||||
BigDecimal applied = entry.getValue();
|
||||
jdbc.update("INSERT INTO fin_receipt_bill(tenant_id,receipt_id,bill_id,applied_amount,created_by) VALUES(?,?,?,?,?)", tenantId, receiptId, entry.getKey(), applied, operatorId);
|
||||
jdbc.update("UPDATE fin_bill SET received_amount=received_amount+?, outstanding_amount=outstanding_amount-?, status=CASE WHEN outstanding_amount-?=0 THEN 'PAID' ELSE 'PARTIALLY_PAID' END, updated_by=?, version=version+1 WHERE id=? AND tenant_id=?", applied, applied, applied, operatorId, entry.getKey(), tenantId);
|
||||
jdbc.update("UPDATE fin_bill SET received_amount=received_amount+?, outstanding_amount=outstanding_amount-?, status=CASE WHEN outstanding_amount=0 THEN 'PAID' ELSE 'PARTIALLY_PAID' END, updated_by=?, version=version+1 WHERE id=? AND tenant_id=?", applied, applied, operatorId, entry.getKey(), tenantId);
|
||||
Integer settlementCount = jdbc.queryForObject(
|
||||
"SELECT COUNT(*) FROM fin_settlement WHERE tenant_id=? AND direction='RECEIVABLE' AND reconciliation_statement_id=? AND status<>'VOIDED'",
|
||||
Integer.class, tenantId, entry.getKey());
|
||||
if (settlementCount != null && settlementCount > 0) {
|
||||
int settlementUpdated = jdbc.update(
|
||||
"UPDATE fin_settlement SET paid_amount=paid_amount+?,outstanding_amount=outstanding_amount-?,payment_status=CASE WHEN outstanding_amount-?=0 THEN 'PAID' ELSE 'PARTIALLY_PAID' END,status=CASE WHEN outstanding_amount-?=0 THEN 'PAID' ELSE 'PARTIALLY_PAID' END,updated_by=?,version=version+1 WHERE tenant_id=? AND direction='RECEIVABLE' AND reconciliation_statement_id=? AND status IN ('ISSUED','PARTIALLY_PAID') AND outstanding_amount>=?",
|
||||
applied, applied, applied, applied, operatorId, tenantId, entry.getKey(), applied);
|
||||
"UPDATE fin_settlement SET paid_amount=paid_amount+?,outstanding_amount=outstanding_amount-?,payment_status=CASE WHEN outstanding_amount=0 THEN 'PAID' ELSE 'PARTIALLY_PAID' END,status=CASE WHEN outstanding_amount=0 THEN 'PAID' ELSE 'PARTIALLY_PAID' END,updated_by=?,version=version+1 WHERE tenant_id=? AND direction='RECEIVABLE' AND reconciliation_statement_id=? AND status IN ('ISSUED','PARTIALLY_PAID') AND outstanding_amount>=?",
|
||||
applied, applied, operatorId, tenantId, entry.getKey(), applied);
|
||||
if (settlementUpdated != 1) throw new BizException(ResultCode.CONFLICT, "应收结算单状态或余额已变化");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +201,9 @@ public class FinanceSettlementService {
|
||||
"SELECT id,tenant_id,direction,currency_code,total_amount,paid_amount,outstanding_amount,status,version " +
|
||||
"FROM fin_settlement WHERE id=? FOR UPDATE", "结算单不存在", settlementId);
|
||||
long customerId = number(settlement, "tenant_id").longValue();
|
||||
Long replay = reserve(customerId, operatorId, "SETTLEMENT_PAYMENT_CREATE", key, "POST",
|
||||
"/api/platform/finance/v1/settlements/" + settlementId + "/transactions", request);
|
||||
if (replay != null) return getSettlement(replay);
|
||||
if (!"PAYABLE".equals(text(settlement, "direction"))) throw error("FIN_409", "仅应付结算单可登记付款");
|
||||
if (!List.of("ISSUED", "PARTIALLY_PAID").contains(text(settlement, "status"))) {
|
||||
throw error("FIN_409", "结算单状态不允许付款");
|
||||
@@ -209,9 +212,6 @@ public class FinanceSettlementService {
|
||||
if (number(settlement, "version").intValue() != request.expectedVersion()) throw error("FIN_409", "结算单版本冲突");
|
||||
BigDecimal outstanding = decimal(settlement, "outstanding_amount");
|
||||
if (request.amount().compareTo(outstanding) > 0) throw error("FIN_409", "付款金额不得超过未付余额");
|
||||
Long replay = reserve(customerId, operatorId, "SETTLEMENT_PAYMENT_CREATE", key, "POST",
|
||||
"/api/platform/finance/v1/settlements/" + settlementId + "/transactions", request);
|
||||
if (replay != null) return getSettlement(settlementId);
|
||||
validateVoucherFiles(customerId, request.voucherFileIds());
|
||||
|
||||
insertPayment(customerId, settlementId, request, operatorId);
|
||||
|
||||
@@ -126,9 +126,11 @@ class FinanceServiceValidationTest {
|
||||
|
||||
service.createReceipt(9L, 2L, request, "receipt-key");
|
||||
|
||||
verify(jdbc).update(startsWith("UPDATE fin_settlement SET paid_amount="),
|
||||
eq(new BigDecimal("40.00")), eq(new BigDecimal("40.00")), eq(new BigDecimal("40.00")),
|
||||
eq(new BigDecimal("40.00")), eq(2L), eq(9L), eq(50L), eq(new BigDecimal("40.00")));
|
||||
verify(jdbc).update(contains("payment_status=CASE WHEN outstanding_amount=0"),
|
||||
eq(new BigDecimal("40.00")), eq(new BigDecimal("40.00")),
|
||||
eq(2L), eq(9L), eq(50L), eq(new BigDecimal("40.00")));
|
||||
verify(jdbc).update(contains("status=CASE WHEN outstanding_amount=0"),
|
||||
eq(new BigDecimal("40.00")), eq(new BigDecimal("40.00")), eq(2L), eq(50L), eq(9L));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -39,6 +39,7 @@ import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.startsWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.RETURNS_DEFAULTS;
|
||||
@@ -276,6 +277,47 @@ class FinanceSettlementBehaviorTest {
|
||||
assertThat(result.summary().outstandingAmount()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void finalPaymentReplayReturnsFirstResultWithoutDuplicatePayment() {
|
||||
JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.class, invocation ->
|
||||
"update".equals(invocation.getMethod().getName()) ? 1 : RETURNS_DEFAULTS.answer(invocation));
|
||||
FinanceFileClient fileClient = org.mockito.Mockito.mock(FinanceFileClient.class);
|
||||
Map<String, Object> before = settlementRow("PAYABLE", "PARTIALLY_PAID", new BigDecimal("40.00"),
|
||||
new BigDecimal("60.00"), 1);
|
||||
Map<String, Object> after = settlementRow("PAYABLE", "PAID", new BigDecimal("100.00"), BigDecimal.ZERO, 2);
|
||||
when(jdbc.queryForList(startsWith("SELECT id,tenant_id,direction"), eq(88L)))
|
||||
.thenReturn(List.of(before), List.of(after));
|
||||
when(jdbc.queryForList(startsWith("SELECT request_hash"), eq(10L), eq(7L),
|
||||
eq("SETTLEMENT_PAYMENT_CREATE"), eq("final-replay-key"))).thenReturn(List.of());
|
||||
stubSettlementDetail(jdbc, after);
|
||||
when(fileClient.metadata("internal-token", 10L, 10L, 91L)).thenReturn(ApiResponse.success(
|
||||
new FinanceFileClient.Metadata(91L, 10L, "FINANCE_VOUCHER", "10", "pay.png", "abc", "image/png", 10L)));
|
||||
FinanceSettlementService service = service(jdbc, fileClient);
|
||||
|
||||
SettlementDetail first = service.createSettlementPayment(88L, 7L, "final-replay-key",
|
||||
payment(new BigDecimal("60.00"), 1));
|
||||
ArgumentCaptor<String> requestHash = ArgumentCaptor.forClass(String.class);
|
||||
verify(jdbc).update(startsWith("INSERT INTO fin_idempotency_record"), eq(10L), eq(7L),
|
||||
eq("SETTLEMENT_PAYMENT_CREATE"), eq("final-replay-key"), requestHash.capture());
|
||||
when(jdbc.queryForList(startsWith("SELECT request_hash"), eq(10L), eq(7L),
|
||||
eq("SETTLEMENT_PAYMENT_CREATE"), eq("final-replay-key"))).thenReturn(List.of(Map.of(
|
||||
"request_hash", requestHash.getValue(), "status", "SUCCEEDED", "result_resource_id", 88L,
|
||||
"updated_at", LocalDateTime.now())));
|
||||
|
||||
SettlementDetail replay = service.createSettlementPayment(88L, 7L, "final-replay-key",
|
||||
payment(new BigDecimal("60.00"), 1));
|
||||
|
||||
assertThat(first.summary().status()).isEqualTo("PAID");
|
||||
assertThat(replay.summary().status()).isEqualTo("PAID");
|
||||
assertThat(replay.summary().outstandingAmount()).isZero();
|
||||
long paymentInserts = org.mockito.Mockito.mockingDetails(jdbc).getInvocations().stream()
|
||||
.filter(invocation -> invocation.getMethod().getName().equals("update")
|
||||
&& invocation.getArguments().length > 0
|
||||
&& String.valueOf((Object) invocation.getArgument(0)).startsWith("INSERT INTO fin_settlement_payment"))
|
||||
.count();
|
||||
assertThat(paymentInserts).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void receivableSettlementDetailIncludesAppliedReceiptHistory() {
|
||||
JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.class);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.baiyue.iam.notification.dto;
|
||||
|
||||
import jakarta.validation.constraints.Future;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class NotificationDtos {
|
||||
private NotificationDtos() { }
|
||||
|
||||
public enum RecipientType { USER, MEMBERSHIP, TENANT_ROLE, TENANT_ALL, PLATFORM_ROLE }
|
||||
|
||||
public record CreateNotificationRequest(
|
||||
@NotNull Long tenantId,
|
||||
@NotBlank @Size(max = 64) String templateCode,
|
||||
@NotNull RecipientType recipientType,
|
||||
@Size(max = 1000) List<Long> recipientIds,
|
||||
@Size(max = 64) String roleCode,
|
||||
@NotNull @Size(max = 32) Map<String, String> variables,
|
||||
@Future LocalDateTime scheduledAt,
|
||||
@NotBlank @Size(min = 8, max = 128) String idempotencyKey,
|
||||
@Size(max = 128) String sourceEventId) { }
|
||||
|
||||
public record NotificationSummary(Long id, String category, String title, String status,
|
||||
LocalDateTime sentAt, LocalDateTime readAt) { }
|
||||
|
||||
public record NotificationDetail(Long id, String category, String title, String body,
|
||||
String status, String sourceType, String sourceId,
|
||||
LocalDateTime sentAt, LocalDateTime readAt) { }
|
||||
|
||||
public record NotificationTaskResponse(Long id, String taskNo, String status, int recipientCount,
|
||||
LocalDateTime scheduledAt, LocalDateTime sentAt,
|
||||
boolean duplicated) { }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.baiyue.iam.notification.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter @Setter
|
||||
@TableName("iam_notification_inbox")
|
||||
public class NotificationInbox {
|
||||
@TableId private Long id;
|
||||
private Long taskId;
|
||||
private Long recipientId;
|
||||
private Long tenantId;
|
||||
private Long membershipId;
|
||||
private String appCode;
|
||||
private String category;
|
||||
private String title;
|
||||
private String body;
|
||||
private String sourceType;
|
||||
private String sourceId;
|
||||
private String status;
|
||||
private LocalDateTime sentAt;
|
||||
private LocalDateTime readAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.baiyue.iam.notification.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter @Setter
|
||||
@TableName("iam_notification_recipient")
|
||||
public class NotificationRecipient {
|
||||
@TableId private Long id;
|
||||
private Long taskId;
|
||||
private Long tenantId;
|
||||
private Long membershipId;
|
||||
private Long userId;
|
||||
private String displayName;
|
||||
private String appCode;
|
||||
private String channel;
|
||||
private String status;
|
||||
private String failureReason;
|
||||
private LocalDateTime deliveredAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.baiyue.iam.notification.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter @Setter
|
||||
@TableName("iam_notification_task")
|
||||
public class NotificationTask {
|
||||
@TableId private Long id;
|
||||
private String taskNo;
|
||||
private Long tenantId;
|
||||
private Long templateId;
|
||||
private String templateCode;
|
||||
private String recipientType;
|
||||
private String variablesJson;
|
||||
private String idempotencyKey;
|
||||
private String sourceEventId;
|
||||
private String sourceEventType;
|
||||
private String status;
|
||||
private Integer recipientCount;
|
||||
private Integer attemptCount;
|
||||
private LocalDateTime scheduledAt;
|
||||
private LocalDateTime sentAt;
|
||||
private String lastError;
|
||||
private Long createdBy;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.baiyue.iam.notification.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter @Setter
|
||||
@TableName("iam_notification_template")
|
||||
public class NotificationTemplate {
|
||||
@TableId private Long id;
|
||||
private Long tenantId;
|
||||
private String templateCode;
|
||||
private String templateName;
|
||||
private String category;
|
||||
private String channel;
|
||||
private String titleTemplate;
|
||||
private String bodyTemplate;
|
||||
private String variableWhitelistJson;
|
||||
private String status;
|
||||
private Integer version;
|
||||
private Long createdBy;
|
||||
private Long updatedBy;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.baiyue.iam.notification.mapper;
|
||||
import com.baiyue.iam.notification.entity.NotificationInbox;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
@Mapper public interface NotificationInboxMapper extends BaseMapper<NotificationInbox> { }
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.baiyue.iam.notification.mapper;
|
||||
import com.baiyue.iam.notification.entity.NotificationRecipient;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
@Mapper public interface NotificationRecipientMapper extends BaseMapper<NotificationRecipient> { }
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.baiyue.iam.notification.mapper;
|
||||
import com.baiyue.iam.notification.entity.NotificationTask;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
@Mapper public interface NotificationTaskMapper extends BaseMapper<NotificationTask> { }
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.baiyue.iam.notification.mapper;
|
||||
import com.baiyue.iam.notification.entity.NotificationTemplate;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
@Mapper public interface NotificationTemplateMapper extends BaseMapper<NotificationTemplate> { }
|
||||
@@ -0,0 +1,113 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: IAM Notification Center API
|
||||
version: 1.0.0
|
||||
description: First release is in-app notifications only. There is no clear-read/delete operation in this release.
|
||||
servers:
|
||||
- url: /api
|
||||
tags:
|
||||
- name: Notification inbox
|
||||
- name: Platform notification administration
|
||||
paths:
|
||||
/{app}/v1/notifications:
|
||||
get:
|
||||
tags: [Notification inbox]
|
||||
summary: List current user's inbox
|
||||
parameters:
|
||||
- {name: app, in: path, required: true, schema: {type: string, enum: [platform, merchant]}}
|
||||
- {name: page, in: query, schema: {type: integer, minimum: 1, default: 1}}
|
||||
- {name: size, in: query, schema: {type: integer, minimum: 1, maximum: 200, default: 20}}
|
||||
- {name: status, in: query, schema: {type: string, enum: [UNREAD, READ]}}
|
||||
- {name: category, in: query, schema: {type: string, maxLength: 64}}
|
||||
responses:
|
||||
'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/InboxPageResponse'}}}}
|
||||
/{app}/v1/notifications/unread-count:
|
||||
get:
|
||||
tags: [Notification inbox]
|
||||
summary: Count current user's unread inbox messages
|
||||
parameters:
|
||||
- {name: app, in: path, required: true, schema: {type: string, enum: [platform, merchant]}}
|
||||
responses:
|
||||
'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/UnreadCountResponse'}}}}
|
||||
/{app}/v1/notifications/{id}:
|
||||
get:
|
||||
tags: [Notification inbox]
|
||||
summary: Get one inbox message
|
||||
parameters:
|
||||
- {name: app, in: path, required: true, schema: {type: string, enum: [platform, merchant]}}
|
||||
- {name: id, in: path, required: true, schema: {type: integer, format: int64}}
|
||||
responses:
|
||||
'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/InboxDetailResponse'}}}}
|
||||
/{app}/v1/notifications/{id}/read:
|
||||
put:
|
||||
tags: [Notification inbox]
|
||||
summary: Mark one message as read (idempotent)
|
||||
parameters:
|
||||
- {name: app, in: path, required: true, schema: {type: string, enum: [platform, merchant]}}
|
||||
- {name: id, in: path, required: true, schema: {type: integer, format: int64}}
|
||||
responses:
|
||||
'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/InboxDetailResponse'}}}}
|
||||
/{app}/v1/notifications/read-all:
|
||||
put:
|
||||
tags: [Notification inbox]
|
||||
summary: Mark all current-user messages as read (idempotent)
|
||||
parameters:
|
||||
- {name: app, in: path, required: true, schema: {type: string, enum: [platform, merchant]}}
|
||||
responses:
|
||||
'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/ReadAllResponse'}}}}
|
||||
/platform/v1/notifications:
|
||||
post:
|
||||
tags: [Platform notification administration]
|
||||
summary: Create an in-app notification task
|
||||
responses:
|
||||
'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/TaskResponse'}}}}
|
||||
/platform/v1/notification-templates:
|
||||
get:
|
||||
tags: [Platform notification administration]
|
||||
summary: List templates
|
||||
parameters: [{name: page, in: query, schema: {type: integer, minimum: 1, default: 1}}, {name: size, in: query, schema: {type: integer, minimum: 1, maximum: 200, default: 20}}, {name: status, in: query, schema: {type: string, enum: [DRAFT, ACTIVE, DISABLED]}}]
|
||||
responses: {'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/TemplatePageResponse'}}}}}
|
||||
post:
|
||||
tags: [Platform notification administration]
|
||||
summary: Create a DRAFT template
|
||||
responses: {'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/TemplateResponse'}}}}}
|
||||
/platform/v1/notification-templates/{id}:
|
||||
put:
|
||||
tags: [Platform notification administration]
|
||||
summary: Edit a DRAFT template only
|
||||
responses: {'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/TemplateResponse'}}}}}
|
||||
/platform/v1/notification-templates/{id}/status:
|
||||
put:
|
||||
tags: [Platform notification administration]
|
||||
summary: Activate or disable a template; only DRAFT->ACTIVE or ACTIVE->DISABLED
|
||||
responses: {'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/TemplateResponse'}}}}}
|
||||
/platform/v1/notification-tasks:
|
||||
get:
|
||||
tags: [Platform notification administration]
|
||||
summary: List tasks
|
||||
parameters: [{name: page, in: query, schema: {type: integer, minimum: 1, default: 1}}, {name: size, in: query, schema: {type: integer, minimum: 1, maximum: 200, default: 20}}, {name: status, in: query, schema: {type: string, enum: [CREATED, SCHEDULED, DISPATCHING, RETRYING, SENT, DEAD, CANCELLED]}}]
|
||||
responses: {'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/TaskPageResponse'}}}}}
|
||||
/platform/v1/notification-tasks/{id}/cancel:
|
||||
put:
|
||||
tags: [Platform notification administration]
|
||||
summary: Cancel CREATED or SCHEDULED task
|
||||
responses: {'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/TaskResponse'}}}}}
|
||||
/platform/v1/notification-tasks/{id}/retry:
|
||||
put:
|
||||
tags: [Platform notification administration]
|
||||
summary: Retry RETRYING or DEAD task
|
||||
responses: {'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/TaskResponse'}}}}}
|
||||
components:
|
||||
schemas:
|
||||
PageMeta: {type: object, required: [page, size, total, pages], properties: {page: {type: integer, format: int64}, size: {type: integer, format: int64}, total: {type: integer, format: int64}, pages: {type: integer, format: int64}}}
|
||||
InboxItem: {type: object, properties: {id: {type: integer, format: int64}, category: {type: string}, title: {type: string}, status: {type: string, enum: [UNREAD, READ]}, sentAt: {type: string, format: date-time}, readAt: {type: string, format: date-time, nullable: true}}}
|
||||
InboxDetail: {allOf: [{$ref: '#/components/schemas/InboxItem'}, {type: object, properties: {body: {type: string}, sourceType: {type: string}, sourceId: {type: string}}}]}
|
||||
InboxPageResponse: {type: object, properties: {code: {type: string, example: '200'}, data: {type: object, properties: {records: {type: array, items: {$ref: '#/components/schemas/InboxItem'}}, page: {type: integer}, size: {type: integer}, total: {type: integer}, pages: {type: integer}}}}}
|
||||
InboxDetailResponse: {type: object, properties: {code: {type: string, example: '200'}, data: {$ref: '#/components/schemas/InboxDetail'}}}
|
||||
UnreadCountResponse: {type: object, properties: {code: {type: string, example: '200'}, data: {type: object, properties: {unreadCount: {type: integer, format: int64}}}}}
|
||||
ReadAllResponse: {type: object, properties: {code: {type: string, example: '200'}, data: {type: object, properties: {updatedCount: {type: integer, format: int64}}}}}
|
||||
CreateNotificationRequest: {type: object, required: [tenantId, templateCode, recipientType, variables, idempotencyKey], properties: {tenantId: {type: integer, format: int64, minimum: 0}, templateCode: {type: string, maxLength: 64}, recipientType: {type: string, enum: [USER, MEMBERSHIP, TENANT_ROLE, TENANT_ALL, PLATFORM_ROLE]}, recipientIds: {type: array, maxItems: 1000, items: {type: integer, format: int64}}, roleCode: {type: string, maxLength: 64}, variables: {type: object, additionalProperties: {type: string}}, scheduledAt: {type: string, format: date-time, nullable: true}, idempotencyKey: {type: string, minLength: 8, maxLength: 128}, sourceEventId: {type: string, maxLength: 128, nullable: true}}}
|
||||
TemplateResponse: {type: object, properties: {id: {type: integer, format: int64}, templateCode: {type: string}, templateName: {type: string}, category: {type: string}, channel: {type: string, enum: [IN_APP]}, titleTemplate: {type: string}, bodyTemplate: {type: string}, variableWhitelist: {type: array, items: {type: string}}, status: {type: string, enum: [DRAFT, ACTIVE, DISABLED]}, version: {type: integer}}}
|
||||
TemplatePageResponse: {type: object, properties: {code: {type: string, example: '200'}, data: {type: object, properties: {records: {type: array, items: {$ref: '#/components/schemas/TemplateResponse'}}, page: {type: integer}, size: {type: integer}, total: {type: integer}, pages: {type: integer}}}}}
|
||||
TaskResponse: {type: object, properties: {id: {type: integer, format: int64}, taskNo: {type: string}, status: {type: string, enum: [CREATED, SCHEDULED, DISPATCHING, RETRYING, SENT, DEAD, CANCELLED]}, recipientCount: {type: integer}, scheduledAt: {type: string, format: date-time, nullable: true}, sentAt: {type: string, format: date-time, nullable: true}, duplicated: {type: boolean}}}
|
||||
TaskPageResponse: {type: object, properties: {code: {type: string, example: '200'}, data: {type: object, properties: {records: {type: array, items: {$ref: '#/components/schemas/TaskResponse'}}, page: {type: integer}, size: {type: integer}, total: {type: integer}, pages: {type: integer}}}}}
|
||||
Reference in New Issue
Block a user