Browse Source

Merge remote-tracking branch 'origin/main'

# Conflicts:
#	logic/all-logic/pom.xml
#	one-application/src/main/java/com/iohao/mmo/OneApplication.java
#	one-client/src/main/java/com/iohao/mmo/client/CommonClient.java
#	pom.xml
#	provide/all-provide/pom.xml
#	provide/common-provide/src/main/java/com/iohao/mmo/common/provide/cmd/CmdModule.java
tangbin 1 year ago
parent
commit
841dcb95d5
51 changed files with 2059 additions and 118 deletions
  1. 61 0
      common/common-core/src/main/java/com/iohao/mmo/common/core/flow/MyFlowContext.java
  2. 97 0
      common/common-core/src/main/java/com/iohao/mmo/common/logic/server/MyUserProcessorExecutorStrategy.java
  3. 7 1
      logic/all-logic/pom.xml
  4. 30 9
      logic/bag-logic/src/main/java/com/iohao/mmo/bag/action/BagAction.java
  5. 1 1
      logic/bag-logic/src/main/java/com/iohao/mmo/bag/action/ItemAction.java
  6. 9 0
      logic/bag-logic/src/main/java/com/iohao/mmo/bag/mapper/BagMapper.java
  7. 24 6
      logic/bag-logic/src/main/java/com/iohao/mmo/bag/service/BagService.java
  8. 3 5
      logic/level-logic/src/main/java/com/iohao/mmo/level/action/LevelAction.java
  9. 38 0
      logic/mail-logic/.gitignore
  10. 34 0
      logic/mail-logic/pom.xml
  11. 55 0
      logic/mail-logic/src/main/java/com/iohao/mmo/mail/MailLogicServer.java
  12. 152 0
      logic/mail-logic/src/main/java/com/iohao/mmo/mail/action/MailAction.java
  13. 83 0
      logic/mail-logic/src/main/java/com/iohao/mmo/mail/entity/Mail.java
  14. 43 0
      logic/mail-logic/src/main/java/com/iohao/mmo/mail/entity/MailAttachment.java
  15. 48 0
      logic/mail-logic/src/main/java/com/iohao/mmo/mail/entity/MailBox.java
  16. 7 7
      logic/mail-logic/src/main/java/com/iohao/mmo/mail/entity/MailStatusEnum.java
  17. 42 0
      logic/mail-logic/src/main/java/com/iohao/mmo/mail/mapper/MailAttachmentMapper.java
  18. 57 0
      logic/mail-logic/src/main/java/com/iohao/mmo/mail/mapper/MailMapper.java
  19. 32 0
      logic/mail-logic/src/main/java/com/iohao/mmo/mail/repository/MailBoxRepository.java
  20. 151 0
      logic/mail-logic/src/main/java/com/iohao/mmo/mail/service/MailBoxService.java
  21. 19 6
      one-application/src/main/java/com/iohao/mmo/OneApplication.java
  22. 16 8
      one-client/src/main/java/com/iohao/mmo/client/CommonClient.java
  23. 4 2
      pom.xml
  24. 7 1
      provide/all-provide/pom.xml
  25. 8 12
      provide/bag-provide/src/main/java/com/iohao/mmo/bag/client/BagExchange.java
  26. 42 25
      provide/bag-provide/src/main/java/com/iohao/mmo/bag/client/BagInputCommandRegion.java
  27. 3 7
      provide/bag-provide/src/main/java/com/iohao/mmo/bag/client/ItemInputCommandRegion.java
  28. 3 1
      provide/bag-provide/src/main/java/com/iohao/mmo/bag/cmd/BagCmd.java
  29. 1 1
      provide/bag-provide/src/main/java/com/iohao/mmo/bag/proto/ItemMessage.java
  30. 7 0
      provide/common-provide/pom.xml
  31. 63 0
      provide/common-provide/src/main/java/com/iohao/mmo/common/provide/client/CommonExchange.java
  32. 57 0
      provide/common-provide/src/main/java/com/iohao/mmo/common/provide/client/CommonInputCommandRegion.java
  33. 6 0
      provide/common-provide/src/main/java/com/iohao/mmo/common/provide/client/ExchangeKit.java
  34. 5 1
      provide/common-provide/src/main/java/com/iohao/mmo/common/provide/cmd/CmdModule.java
  35. 34 0
      provide/common-provide/src/main/java/com/iohao/mmo/common/provide/cmd/CommonCmd.java
  36. 49 0
      provide/common-provide/src/main/java/com/iohao/mmo/common/provide/kit/ItemKit.java
  37. 28 0
      provide/common-provide/src/main/java/com/iohao/mmo/common/provide/kit/ItemNode.java
  38. 63 0
      provide/common-provide/src/main/java/com/iohao/mmo/common/provide/kit/ItemNodeMap.java
  39. 4 0
      provide/common-provide/src/main/java/com/iohao/mmo/common/provide/kit/JsonKit.java
  40. 11 23
      provide/common-provide/src/main/java/com/iohao/mmo/common/provide/proto/ShowItemMessage.java
  41. 2 2
      provide/level-provide/src/main/java/com/iohao/mmo/level/client/LevelInputCommandRegion.java
  42. 38 0
      provide/mail-provide/.gitignore
  43. 33 0
      provide/mail-provide/pom.xml
  44. 57 0
      provide/mail-provide/src/main/java/com/iohao/mmo/mail/client/MailExchange.java
  45. 154 0
      provide/mail-provide/src/main/java/com/iohao/mmo/mail/client/MailInputCommandRegion.java
  46. 49 0
      provide/mail-provide/src/main/java/com/iohao/mmo/mail/cmd/MailCmd.java
  47. 143 0
      provide/mail-provide/src/main/java/com/iohao/mmo/mail/kit/InternalMailBuilder.java
  48. 40 0
      provide/mail-provide/src/main/java/com/iohao/mmo/mail/proto/InternalMailMessage.java
  49. 41 0
      provide/mail-provide/src/main/java/com/iohao/mmo/mail/proto/MailAttachmentMessage.java
  50. 65 0
      provide/mail-provide/src/main/java/com/iohao/mmo/mail/proto/MailMessage.java
  51. 33 0
      provide/mail-provide/src/main/java/com/iohao/mmo/mail/proto/MailStatusMessageEnum.java

+ 61 - 0
common/common-core/src/main/java/com/iohao/mmo/common/core/flow/MyFlowContext.java

@@ -18,7 +18,14 @@
  */
 package com.iohao.mmo.common.core.flow;
 
+import com.iohao.game.action.skeleton.core.CmdInfo;
+import com.iohao.game.action.skeleton.core.commumication.BroadcastContext;
+import com.iohao.game.action.skeleton.core.commumication.BrokerClientContext;
 import com.iohao.game.action.skeleton.core.flow.FlowContext;
+import com.iohao.game.action.skeleton.core.flow.attr.FlowAttr;
+import com.iohao.game.action.skeleton.protocol.HeadMetadata;
+import com.iohao.game.action.skeleton.protocol.RequestMessage;
+import com.iohao.game.action.skeleton.protocol.ResponseMessage;
 import com.iohao.game.common.kit.ArrayKit;
 
 import java.util.Objects;
@@ -56,4 +63,58 @@ public class MyFlowContext extends FlowContext {
             this.attachment = this.getAttachment(MyAttachment.class);
         }
     }
+
+    /**
+     * 推送数据给玩家
+     *
+     * @param cmdInfo 路由
+     * @param data    业务数据
+     */
+    public void broadcast(CmdInfo cmdInfo, Object data) {
+
+        ResponseMessage responseMessage = createResponseMessage(cmdInfo, data);
+
+        BrokerClientContext brokerClientContext = this.option(FlowAttr.brokerClientContext);
+        BroadcastContext broadcastContext = brokerClientContext.getBroadcastContext();
+        // 为了方便理解,这里加上 userId;
+        broadcastContext.broadcast(responseMessage, getUserId());
+    }
+
+    @Override
+    protected RequestMessage createRequestMessage(CmdInfo cmdInfo, Object data) {
+
+        HeadMetadata headMetadata = getRequest().getHeadMetadata();
+
+        RequestMessage requestMessage = super.createRequestMessage(cmdInfo, data);
+
+        requestMessage.getHeadMetadata()
+                .setEndPointClientId(headMetadata.getEndPointClientId())
+                .setSourceClientId(headMetadata.getSourceClientId())
+        ;
+
+        return requestMessage;
+    }
+
+
+    private ResponseMessage createResponseMessage(CmdInfo cmdInfo, Object data) {
+        // 创建一个响应对象
+        RequestMessage request = this.getRequest();
+        HeadMetadata headMetadata = request.getHeadMetadata();
+        /*
+         * 创建一个 HeadMetadata,并使用原有的一些信息;
+         * 在广播时,只会给 HeadMetadata 中指定的游戏对外服广播。
+         */
+        HeadMetadata headMetadataClone = headMetadata
+                .cloneHeadMetadata()
+                .setCmdInfo(cmdInfo)
+                .setEndPointClientId(headMetadata.getEndPointClientId())
+                .setSourceClientId(headMetadata.getSourceClientId());
+
+        ResponseMessage responseMessage = new ResponseMessage();
+        responseMessage.setHeadMetadata(headMetadataClone);
+        responseMessage.setData(data);
+
+        return responseMessage;
+    }
+
 }

+ 97 - 0
common/common-core/src/main/java/com/iohao/mmo/common/logic/server/MyUserProcessorExecutorStrategy.java

@@ -0,0 +1,97 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.common.logic.server;
+
+import com.iohao.game.bolt.broker.core.aware.UserProcessorExecutorAware;
+import com.iohao.game.bolt.broker.core.common.UserProcessorExecutorStrategy;
+import com.iohao.game.common.kit.concurrent.DaemonThreadFactory;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * 自定义 UserProcessor 构建 Executor 的策略
+ *
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@Slf4j
+public class MyUserProcessorExecutorStrategy implements UserProcessorExecutorStrategy {
+    final AtomicInteger id = new AtomicInteger();
+    final Executor commonExecutor;
+
+    Map<String, Executor> executorMap = new ConcurrentHashMap<>();
+
+    public MyUserProcessorExecutorStrategy() {
+        int corePoolSize = Runtime.getRuntime().availableProcessors();
+        int maximumPoolSize = corePoolSize << 1;
+        this.commonExecutor = createExecutor("common", corePoolSize, maximumPoolSize);
+    }
+
+    @Override
+    public Executor getExecutor(UserProcessorExecutorAware userProcessorExecutorAware) {
+        String userProcessorName = userProcessorExecutorAware.getClass().getSimpleName();
+
+        return switch (userProcessorName) {
+            case "RequestMessageClientProcessor", "SettingUserIdMessageExternalProcessor" ->
+                    this.createExecutor(userProcessorName);
+            // 其他类型的消息处理共用一个池
+            default -> this.commonExecutor;
+        };
+    }
+
+    Executor createExecutor(String userProcessorName) {
+        int corePoolSize = Runtime.getRuntime().availableProcessors();
+        return createExecutor(userProcessorName, corePoolSize, corePoolSize);
+    }
+
+    Executor createExecutor(String userProcessorName, int corePoolSize, int maximumPoolSize) {
+
+        Executor executor = executorMap.get(userProcessorName);
+        if (Objects.isNull(executor)) {
+            String namePrefix = String.format("Executor-%s-%d"
+                    , userProcessorName
+                    , id.incrementAndGet());
+
+            DaemonThreadFactory threadFactory = new DaemonThreadFactory(namePrefix);
+            executor = new ThreadPoolExecutor(
+                    corePoolSize, maximumPoolSize,
+                    60L, TimeUnit.SECONDS,
+                    new LinkedBlockingQueue<>(),
+                    threadFactory);
+
+            executor = executorMap.putIfAbsent(userProcessorName, executor);
+
+            if (Objects.isNull(executor)) {
+                executor = executorMap.get(userProcessorName);
+            }
+
+            // 小预热
+            for (int i = 0; i < corePoolSize; i++) {
+                executor.execute(() -> {
+                });
+            }
+        }
+
+        return executor;
+    }
+}

+ 7 - 1
logic/all-logic/pom.xml

@@ -48,6 +48,12 @@
             <artifactId>bag-logic</artifactId>
             <version>${project.parent.version}</version>
         </dependency>
+
+        <dependency>
+            <groupId>com.iohao.mmo</groupId>
+            <artifactId>mail-logic</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
         <dependency>
             <groupId>com.iohao.mmo</groupId>
             <artifactId>equip-logic</artifactId>
@@ -56,4 +62,4 @@
 
     </dependencies>
 
-</project>
+</project>

+ 30 - 9
logic/bag-logic/src/main/java/com/iohao/mmo/bag/action/BagAction.java

@@ -20,8 +20,11 @@ package com.iohao.mmo.bag.action;
 
 import com.iohao.game.action.skeleton.annotation.ActionController;
 import com.iohao.game.action.skeleton.annotation.ActionMethod;
+import com.iohao.game.action.skeleton.core.CmdInfo;
 import com.iohao.game.action.skeleton.core.exception.ActionErrorEnum;
 import com.iohao.game.action.skeleton.core.flow.FlowContext;
+import com.iohao.game.action.skeleton.protocol.wrapper.WrapperKit;
+import com.iohao.game.common.kit.CollKit;
 import com.iohao.mmo.bag.cmd.BagCmd;
 import com.iohao.mmo.bag.entity.Bag;
 import com.iohao.mmo.bag.entity.BagItem;
@@ -35,10 +38,15 @@ import com.iohao.mmo.bag.region.SceneConst;
 import com.iohao.mmo.bag.region.UseContext;
 import com.iohao.mmo.bag.region.UseRegion;
 import com.iohao.mmo.bag.service.BagService;
+import com.iohao.mmo.common.core.flow.MyFlowContext;
+import com.iohao.mmo.common.provide.client.CommonExchange;
+import com.iohao.mmo.common.provide.proto.ShowItemMessage;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
+import java.util.*;
+
 /**
  * 背包
  *
@@ -69,22 +77,35 @@ public class BagAction {
     }
 
     /**
-     * 往背包添加(增加)物品
+     * 往背包添加(增加)多个物品
      *
-     * @param bagItemMessage 物品
-     * @param flowContext    flowContext
-     * @return 当前物品的最新信息
+     * @param bagItemMessages 物品列表
+     * @param flowContext     flowContext
      */
     @ActionMethod(BagCmd.incrementItem)
-    public BagItemMessage internalIncrementItem(BagItemMessage bagItemMessage, FlowContext flowContext) {
+    public void internalIncrementItems(List<BagItemMessage> bagItemMessages, MyFlowContext flowContext) {
+
+        if (CollKit.isEmpty(bagItemMessages)) {
+            return;
+        }
+
         long userId = flowContext.getUserId();
 
-        BagItem bagItem = BagMapper.ME.convert(bagItemMessage);
+        // 增加多个物品
+        List<BagItem> bagItems = BagMapper.ME.convertBagItems(bagItemMessages);
+        bagItems = this.bagService.incrementItems(bagItems, userId);
 
-        bagItem = bagService.incrementItem(bagItem, userId);
-        BagMapper.ME.to(bagItem, bagItemMessage);
+        // 推送物品变动列表
+        List<BagItemMessage> list = BagMapper.ME.convertBagItemMessages(bagItems);
+        CmdInfo cmdInfo = BagCmd.of(BagCmd.broadcastChangeItems);
+        flowContext.broadcast(cmdInfo, WrapperKit.ofListByteValue(list));
 
-        return bagItemMessage;
+        // 物品获得通知
+        List<ShowItemMessage> showItemMessageList = bagItemMessages.stream()
+                .map(BagMapper.ME::convertShowItem)
+                .toList();
+
+        CommonExchange.broadcastShowItem(showItemMessageList, flowContext);
     }
 
     /**

+ 1 - 1
logic/bag-logic/src/main/java/com/iohao/mmo/bag/action/ItemAction.java

@@ -35,7 +35,7 @@ import java.util.List;
  * 物品配置相关
  *
  * @author 渔民小镇
- * @date 2023-08-13
+ * @date 2023-08-15
  */
 @Slf4j
 @Component

+ 9 - 0
logic/bag-logic/src/main/java/com/iohao/mmo/bag/mapper/BagMapper.java

@@ -22,10 +22,13 @@ import com.iohao.mmo.bag.entity.Bag;
 import com.iohao.mmo.bag.entity.BagItem;
 import com.iohao.mmo.bag.proto.BagItemMessage;
 import com.iohao.mmo.bag.proto.BagMessage;
+import com.iohao.mmo.common.provide.proto.ShowItemMessage;
 import org.mapstruct.Mapper;
 import org.mapstruct.MappingTarget;
 import org.mapstruct.factory.Mappers;
 
+import java.util.List;
+
 /**
  * @author 渔民小镇
  * @date 2023-08-04
@@ -40,5 +43,11 @@ public interface BagMapper {
 
     BagItem convert(BagItemMessage bagItemMessage);
 
+    List<BagItem> convertBagItems(List<BagItemMessage> bagItemMessages);
+
+    List<BagItemMessage> convertBagItemMessages(List<BagItem> bagItems);
+
     void to(BagItem bagItem, @MappingTarget BagItemMessage bagItemMessage);
+
+    ShowItemMessage convertShowItem(BagItemMessage bagItemMessage);
 }

+ 24 - 6
logic/bag-logic/src/main/java/com/iohao/mmo/bag/service/BagService.java

@@ -21,7 +21,6 @@ package com.iohao.mmo.bag.service;
 import com.iohao.game.action.skeleton.core.exception.ActionErrorEnum;
 import com.iohao.mmo.bag.entity.Bag;
 import com.iohao.mmo.bag.entity.BagItem;
-import com.iohao.mmo.bag.region.UseContext;
 import com.iohao.mmo.bag.repository.BagRepository;
 import com.iohao.mmo.common.config.GameCode;
 import lombok.AllArgsConstructor;
@@ -56,14 +55,37 @@ public class BagService {
         return bag;
     }
 
+    public List<BagItem> incrementItems(List<BagItem> incrementItems, long userId) {
+        // merge 让多个相同的物品做整合
+        Map<String, BagItem> bagItemMessageMap = new HashMap<>();
+        incrementItems.forEach(incrementItem -> {
+            String key = incrementItem.getItemId();
+            BagItem bagItem = bagItemMessageMap.get(key);
+            if (Objects.isNull(bagItem)) {
+                bagItemMessageMap.put(key, incrementItem);
+            } else {
+                int quantity = incrementItem.getQuantity();
+                bagItem.addQuantity(Math.abs(quantity));
+            }
+        });
+
+        // 物品变更
+        return bagItemMessageMap.values().stream()
+                .filter(bagItem -> bagItem.getQuantity() > 0)
+                .map(bagItem -> this.incrementItem(bagItem, userId))
+                .toList();
+    }
+
     public BagItem incrementItem(BagItem incrementItem, long userId) {
+        int quantity = incrementItem.getQuantity();
+        GameCode.quantityNotEnough.assertTrue(quantity > 0);
+
         Bag bag = ofBag(userId);
         Map<String, BagItem> itemMap = bag.getItemMap();
 
         String bagItemId = incrementItem.getId();
         BagItem bagItem = itemMap.get(bagItemId);
 
-        int quantity = incrementItem.getQuantity();
         if (Objects.isNull(bagItem)) {
             bagItem = incrementItem;
             itemMap.put(bagItemId, bagItem);
@@ -125,8 +147,4 @@ public class BagService {
 
         return true;
     }
-
-    public void use(UseContext context) {
-
-    }
 }

+ 3 - 5
logic/level-logic/src/main/java/com/iohao/mmo/level/action/LevelAction.java

@@ -21,11 +21,10 @@ package com.iohao.mmo.level.action;
 import com.iohao.game.action.skeleton.annotation.ActionController;
 import com.iohao.game.action.skeleton.annotation.ActionMethod;
 import com.iohao.game.action.skeleton.core.CmdInfo;
-import com.iohao.game.action.skeleton.core.commumication.BroadcastContext;
 import com.iohao.game.action.skeleton.core.exception.ActionErrorEnum;
 import com.iohao.game.action.skeleton.core.flow.FlowContext;
-import com.iohao.game.bolt.broker.core.client.BrokerClientHelper;
 import com.iohao.mmo.common.config.GameCode;
+import com.iohao.mmo.common.core.flow.MyFlowContext;
 import com.iohao.mmo.level.cmd.LevelCmd;
 import com.iohao.mmo.level.entity.Level;
 import com.iohao.mmo.level.entity.PersonLevelConfig;
@@ -56,7 +55,7 @@ public class LevelAction {
      * @param expMessage 经验值
      */
     @ActionMethod(LevelCmd.personAddExp)
-    public void internalAddExpPerson(ExpMessage expMessage) {
+    public void internalAddExpPerson(ExpMessage expMessage, MyFlowContext flowContext) {
         // internal 打头的方法名表示内部方法,只能由内部调用
         long userId = expMessage.id;
         int exp = expMessage.exp;
@@ -67,9 +66,8 @@ public class LevelAction {
 
         // 推送经验值给玩家
         LevelMessage levelMessage = LevelMapper.ME.convert(level);
-        BroadcastContext broadcastContext = BrokerClientHelper.getBroadcastContext();
         CmdInfo cmdInfo = LevelCmd.of(LevelCmd.personAddExp);
-        broadcastContext.broadcast(cmdInfo, levelMessage, userId);
+        flowContext.broadcast(cmdInfo, levelMessage);
     }
 
     /**

+ 38 - 0
logic/mail-logic/.gitignore

@@ -0,0 +1,38 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store

+ 34 - 0
logic/mail-logic/pom.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.iohao.mmo</groupId>
+        <artifactId>game</artifactId>
+        <version>1.0-SNAPSHOT</version>
+        <relativePath>../../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>mail-logic</artifactId>
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+    <dependencies>
+        <!-- 游戏逻辑服通用模块 -->
+        <dependency>
+            <groupId>com.iohao.mmo</groupId>
+            <artifactId>a-logic-common</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.iohao.mmo</groupId>
+            <artifactId>mail-provide</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 55 - 0
logic/mail-logic/src/main/java/com/iohao/mmo/mail/MailLogicServer.java

@@ -0,0 +1,55 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.mail;
+
+import com.iohao.game.action.skeleton.core.BarSkeleton;
+import com.iohao.game.action.skeleton.core.BarSkeletonBuilder;
+import com.iohao.game.action.skeleton.core.doc.ActionSendDoc;
+import com.iohao.game.bolt.broker.client.AbstractBrokerClientStartup;
+import com.iohao.game.bolt.broker.core.client.BrokerClientBuilder;
+import com.iohao.mmo.common.logic.server.LogicServerKit;
+import com.iohao.mmo.mail.action.MailAction;
+
+/**
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+public class MailLogicServer extends AbstractBrokerClientStartup {
+    @Override
+    public BarSkeleton createBarSkeleton() {
+        // 业务框架构建器
+        BarSkeletonBuilder builder = LogicServerKit
+                .createBuilder(MailAction.class);
+
+        extractedSendDco(builder);
+
+        return builder.build();
+    }
+
+    @Override
+    public BrokerClientBuilder createBrokerClientBuilder() {
+        BrokerClientBuilder builder = LogicServerKit.newBrokerClientBuilder();
+        builder.appName("邮件逻辑服");
+        return builder;
+    }
+
+    private void extractedSendDco(BarSkeletonBuilder builder) {
+
+    }
+}

+ 152 - 0
logic/mail-logic/src/main/java/com/iohao/mmo/mail/action/MailAction.java

@@ -0,0 +1,152 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.mail.action;
+
+import com.iohao.game.action.skeleton.annotation.ActionController;
+import com.iohao.game.action.skeleton.annotation.ActionMethod;
+import com.iohao.game.action.skeleton.core.CmdInfo;
+import com.iohao.game.action.skeleton.core.commumication.BroadcastContext;
+import com.iohao.game.action.skeleton.core.exception.ActionErrorEnum;
+import com.iohao.game.action.skeleton.core.flow.FlowContext;
+import com.iohao.game.bolt.broker.core.client.BrokerClientHelper;
+import com.iohao.game.common.kit.CollKit;
+import com.iohao.mmo.mail.cmd.MailCmd;
+import com.iohao.mmo.mail.entity.Mail;
+import com.iohao.mmo.mail.entity.MailBox;
+import com.iohao.mmo.mail.mapper.MailMapper;
+import com.iohao.mmo.mail.proto.InternalMailMessage;
+import com.iohao.mmo.mail.proto.MailMessage;
+import com.iohao.mmo.mail.proto.MailStatusMessageEnum;
+import com.iohao.mmo.mail.service.MailBoxService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.bson.types.ObjectId;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@Slf4j
+@Component
+@ActionController(MailCmd.cmd)
+public class MailAction {
+    @Resource
+    MailBoxService mailBoxService;
+
+    /**
+     * 查看玩家邮件列表
+     *
+     * @param flowContext flowContext
+     * @return 玩家邮件
+     */
+    @ActionMethod(MailCmd.listMail)
+    public List<MailMessage> listMail(FlowContext flowContext) {
+        long userId = flowContext.getUserId();
+        MailBox mailBox = mailBoxService.ofMailBox(userId);
+
+        return MailMapper.ME.convertMails(mailBox.getMails());
+    }
+
+    /**
+     * 批量添加新邮件
+     *
+     * @param internalMailMessages 邮件
+     */
+    @ActionMethod(MailCmd.addMail)
+    public void internalAddMail(List<InternalMailMessage> internalMailMessages) {
+        if (CollKit.isEmpty(internalMailMessages)) {
+            return;
+        }
+
+        BroadcastContext broadcastContext = BrokerClientHelper.getBroadcastContext();
+
+        internalMailMessages.stream()
+                .filter(internalMail -> internalMail.userId > 0 && Objects.nonNull(internalMail.mailMessage))
+                .forEach(internalMailMessage -> {
+                    long userId = internalMailMessage.userId;
+                    MailMessage mailMessage = internalMailMessage.mailMessage;
+
+                    mailMessage.mailStatus = MailStatusMessageEnum.SEAL;
+                    mailMessage.id = new ObjectId().toString();
+
+                    Mail mail = MailMapper.ME.convert(mailMessage);
+                    this.mailBoxService.addMail(mail, userId);
+
+                    // 新邮件通知
+                    CmdInfo cmdInfo = MailCmd.of(MailCmd.sendNewMail);
+                    broadcastContext.broadcast(cmdInfo, mailMessage, userId);
+                });
+    }
+
+    /**
+     * 删除单个邮件-指定邮件
+     *
+     * @param mailId      email Id
+     * @param flowContext flowContext
+     * @return true 表示删除成功
+     */
+    @ActionMethod(MailCmd.deleteMail)
+    public boolean deleteMail(String mailId, FlowContext flowContext) {
+        long userId = flowContext.getUserId();
+
+        boolean result = mailBoxService.deleteMail(mailId, userId);
+
+        ActionErrorEnum.dataNotExist.assertTrue(result);
+
+        return result;
+    }
+
+    /**
+     * 一键删除多个邮件,删除所有已开封和过期的邮件
+     *
+     * @param flowContext flowContext
+     * @return true;如要需要可以重读一次 listMail
+     */
+    @ActionMethod(MailCmd.deleteMails)
+    public boolean deleteMails(FlowContext flowContext) {
+        long userId = flowContext.getUserId();
+        mailBoxService.deleteMails(userId);
+        return true;
+    }
+
+    /**
+     * 领取指定未开封的邮件
+     *
+     * @param mailId      邮件 id
+     * @param flowContext flowContext
+     */
+    @ActionMethod(MailCmd.openMail)
+    public void openMail(String mailId, FlowContext flowContext) {
+        this.mailBoxService.openMail(mailId, flowContext);
+    }
+
+    /**
+     * 一键领取所有未开封的邮件
+     *
+     * @param flowContext flowContext
+     */
+    @ActionMethod(MailCmd.openMails)
+    public void openMails(FlowContext flowContext) {
+        this.mailBoxService.openMail(flowContext);
+    }
+}

+ 83 - 0
logic/mail-logic/src/main/java/com/iohao/mmo/mail/entity/Mail.java

@@ -0,0 +1,83 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.mail.entity;
+
+import com.iohao.game.common.kit.CollKit;
+import com.iohao.game.common.kit.TimeKit;
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 玩家邮件
+ *
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@Data
+@Document
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class Mail {
+    @Id
+    String id;
+    /** 发件人 */
+    String senderName;
+    /** 发件人 userId */
+    long senderUserId;
+    /** 邮件主题 */
+    String subject;
+    /** 邮件正文 */
+    String body;
+    /** 发送时间 */
+    long milliseconds;
+    /** 过期时间 */
+    long expiredMilliseconds;
+    /** 邮件状态 */
+    MailStatusEnum mailStatus;
+    /** 附件(奖励) */
+    List<MailAttachment> mailAttachments;
+
+    public void addMailAttachment(MailAttachment mailAttachment) {
+        if (Objects.isNull(mailAttachments)) {
+            this.mailAttachments = new ArrayList<>();
+        }
+
+        this.mailAttachments.add(mailAttachment);
+    }
+
+    /**
+     * @return true 表示邮件没有附件
+     */
+    public boolean isEmpty() {
+        return CollKit.isEmpty(this.mailAttachments);
+    }
+
+    /**
+     * @return true 表示邮件已经过期
+     */
+    public boolean isExpire() {
+        return TimeKit.expire(expiredMilliseconds);
+    }
+}

+ 43 - 0
logic/mail-logic/src/main/java/com/iohao/mmo/mail/entity/MailAttachment.java

@@ -0,0 +1,43 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.mail.entity;
+
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+/**
+ * 邮件附件(奖励)
+ *
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@Data
+@Document
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class MailAttachment {
+    @Id
+    String id;
+    /** 物品 id */
+    String itemId;
+    /** 物品数量 */
+    int quantity;
+}

+ 48 - 0
logic/mail-logic/src/main/java/com/iohao/mmo/mail/entity/MailBox.java

@@ -0,0 +1,48 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.mail.entity;
+
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+import java.util.List;
+
+/**
+ * 玩家邮箱
+ *
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@Data
+@Document
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class MailBox {
+    /** userId */
+    @Id
+    long userId;
+    /** 邮件列表 */
+    List<Mail> mails;
+
+    public void addMail(Mail mail) {
+        this.mails.add(mail);
+    }
+}

+ 7 - 7
provide/bag-provide/src/main/java/com/iohao/mmo/bag/client/ext/ClientBagAttr.java → logic/mail-logic/src/main/java/com/iohao/mmo/mail/entity/MailStatusEnum.java

@@ -16,15 +16,15 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
-package com.iohao.mmo.bag.client.ext;
-
-import com.iohao.game.common.kit.attr.AttrOption;
+package com.iohao.mmo.mail.entity;
 
 /**
  * @author 渔民小镇
- * @date 2023-08-13
+ * @date 2023-08-15
  */
-public interface ClientBagAttr {
-    AttrOption<ItemMessageMap> itemMessageMapAttrOption = AttrOption.valueOf("itemMessageMapAttrOption");
-
+public enum MailStatusEnum {
+    /** 密封的邮件、未开封的邮件 */
+    SEAL,
+    /** 已开封的邮件、奖励已经被领取了 */
+    OPEN;
 }

+ 42 - 0
logic/mail-logic/src/main/java/com/iohao/mmo/mail/mapper/MailAttachmentMapper.java

@@ -0,0 +1,42 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.mail.mapper;
+
+import com.iohao.mmo.bag.proto.BagItemMessage;
+import com.iohao.mmo.mail.entity.MailAttachment;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+/**
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@Mapper
+public interface MailAttachmentMapper {
+    MailAttachmentMapper ME = Mappers.getMapper(MailAttachmentMapper.class);
+
+    @Mapping(source = "itemId", target = "id")
+    BagItemMessage convert(MailAttachment mailAttachment);
+
+    List<BagItemMessage> convertBagItems(List<MailAttachment> mailAttachments);
+
+}

+ 57 - 0
logic/mail-logic/src/main/java/com/iohao/mmo/mail/mapper/MailMapper.java

@@ -0,0 +1,57 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.mail.mapper;
+
+import com.iohao.mmo.mail.entity.Mail;
+import com.iohao.mmo.mail.entity.MailStatusEnum;
+import com.iohao.mmo.mail.proto.MailMessage;
+import com.iohao.mmo.mail.proto.MailStatusMessageEnum;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+/**
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@Mapper
+public interface MailMapper {
+    MailMapper ME = Mappers.getMapper(MailMapper.class);
+
+    MailMessage convert(Mail mail);
+
+    Mail convert(MailMessage mailMessage);
+
+    List<MailMessage> convertMails(List<Mail> mails);
+
+    default MailStatusMessageEnum convert(MailStatusEnum mailStatusEnum) {
+        return switch (mailStatusEnum) {
+            case OPEN -> MailStatusMessageEnum.OPEN;
+            case SEAL -> MailStatusMessageEnum.SEAL;
+        };
+    }
+
+    default MailStatusEnum convert(MailStatusMessageEnum mailStatusMessageEnum) {
+        return switch (mailStatusMessageEnum) {
+            case OPEN -> MailStatusEnum.OPEN;
+            case SEAL -> MailStatusEnum.SEAL;
+        };
+    }
+}

+ 32 - 0
logic/mail-logic/src/main/java/com/iohao/mmo/mail/repository/MailBoxRepository.java

@@ -0,0 +1,32 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.mail.repository;
+
+import com.iohao.mmo.mail.entity.MailBox;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@Repository
+public interface MailBoxRepository extends CrudRepository<MailBox, Long> {
+
+}

+ 151 - 0
logic/mail-logic/src/main/java/com/iohao/mmo/mail/service/MailBoxService.java

@@ -0,0 +1,151 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.mail.service;
+
+import com.iohao.game.action.skeleton.core.exception.ActionErrorEnum;
+import com.iohao.game.action.skeleton.core.flow.FlowContext;
+import com.iohao.game.common.kit.CollKit;
+import com.iohao.mmo.bag.client.BagExchange;
+import com.iohao.mmo.bag.proto.BagItemMessage;
+import com.iohao.mmo.mail.entity.Mail;
+import com.iohao.mmo.mail.entity.MailBox;
+import com.iohao.mmo.mail.entity.MailStatusEnum;
+import com.iohao.mmo.mail.mapper.MailAttachmentMapper;
+import com.iohao.mmo.mail.repository.MailBoxRepository;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+/**
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@Service
+@AllArgsConstructor
+public class MailBoxService {
+    final MailBoxRepository mailBoxRepository;
+
+    public MailBox ofMailBox(long userId) {
+        return mailBoxRepository.findById(userId).orElseGet(() -> {
+            MailBox mailBox = new MailBox();
+            mailBox.setUserId(userId);
+
+            mailBox.setMails(new ArrayList<>());
+
+            mailBoxRepository.save(mailBox);
+            return mailBox;
+        });
+    }
+
+    /**
+     * 添加 Mail
+     *
+     * @param mail   Mail
+     * @param userId 玩家 id
+     */
+    public void addMail(Mail mail, long userId) {
+        MailBox mailBox = ofMailBox(userId);
+        mailBox.addMail(mail);
+
+        mailBoxRepository.save(mailBox);
+    }
+
+    public boolean deleteMail(String mailId, long userId) {
+        // 删除指定邮件
+        MailBox mailBox = ofMailBox(userId);
+
+        List<Mail> mails = mailBox.getMails();
+        boolean result = mails.removeIf(mail -> mail.getId().equals(mailId));
+
+        if (result) {
+            mailBoxRepository.save(mailBox);
+        }
+
+        return result;
+    }
+
+    public void deleteMails(long userId) {
+        // 删除所有已开封和过期的邮件
+        MailBox mailBox = ofMailBox(userId);
+
+        boolean result = mailBox.getMails().removeIf(mail -> {
+            // 是否已开封的邮件
+            return mail.getMailStatus() == MailStatusEnum.OPEN
+                    // 是否过期邮件
+                    || mail.isExpire();
+        });
+
+        if (result) {
+            mailBoxRepository.save(mailBox);
+        }
+    }
+
+    public void openMail(String mailId, FlowContext flowContext) {
+        long userId = flowContext.getUserId();
+        MailBox mailBox = ofMailBox(userId);
+
+        Optional<Mail> optionalMail = mailBox.getMails()
+                .stream()
+                .filter(mail -> Objects.equals(mail.getId(), mailId))
+                .filter(mail -> mail.getMailStatus() == MailStatusEnum.SEAL)
+                .findFirst();
+
+        ActionErrorEnum.dataNotExist.assertTrue(optionalMail.isPresent(), "邮件不存在或已领取");
+
+        optionalMail.ifPresent(mail -> openMail(List.of(mail), mailBox, flowContext));
+    }
+
+    public void openMail(FlowContext flowContext) {
+        long userId = flowContext.getUserId();
+        MailBox mailBox = ofMailBox(userId);
+
+        // 密封的邮件、未开封的邮件
+        List<Mail> list = mailBox.getMails().stream()
+                .filter(mail -> mail.getMailStatus() == MailStatusEnum.SEAL)
+                .toList();
+
+        ActionErrorEnum.dataNotExist.assertTrueThrows(list.isEmpty(), "邮件不存在或已领取");
+
+        openMail(list, mailBox, flowContext);
+    }
+
+    private void openMail(List<Mail> processMails, MailBox mailBox, FlowContext flowContext) {
+        // 需要处理的邮件列表
+        processMails.forEach(mail -> mail.setMailStatus(MailStatusEnum.OPEN));
+        this.mailBoxRepository.save(mailBox);
+
+        // 将邮件附件转为背包物品
+        List<BagItemMessage> list = processMails.stream()
+                // 需要有附件的邮件
+                .filter(mail -> !mail.isEmpty())
+                // 需要是未过期的邮件
+                .filter(mail -> !mail.isExpire())
+                // 邮件附件
+                .flatMap(mail -> mail.getMailAttachments().stream())
+                // 将附件转为背包物品
+                .map(MailAttachmentMapper.ME::convert)
+                .toList();
+
+        if (CollKit.notEmpty(list)) {
+            // 调用背包模块,增加物品
+            BagExchange.incrementItems(list, flowContext);
+        }
+    }
+}

+ 19 - 6
one-application/src/main/java/com/iohao/mmo/OneApplication.java

@@ -20,16 +20,19 @@ package com.iohao.mmo;
 
 import com.iohao.game.action.skeleton.ext.spring.ActionFactoryBeanForSpring;
 import com.iohao.game.bolt.broker.client.AbstractBrokerClientStartup;
+import com.iohao.game.bolt.broker.core.common.IoGameGlobalConfig;
 import com.iohao.game.bolt.broker.server.BrokerServer;
 import com.iohao.game.external.core.ExternalServer;
 import com.iohao.game.external.core.config.ExternalGlobalConfig;
 import com.iohao.game.external.core.netty.simple.NettyRunOne;
 import com.iohao.mmo.bag.BagLogicServer;
 import com.iohao.mmo.broker.MyBrokerServer;
+import com.iohao.mmo.common.logic.server.MyUserProcessorExecutorStrategy;
 import com.iohao.mmo.equip.EquipLogicServer;
 import com.iohao.mmo.external.MyExternalServer;
 import com.iohao.mmo.level.LevelLogicServer;
 import com.iohao.mmo.login.LoginLogicServer;
+import com.iohao.mmo.mail.MailLogicServer;
 import com.iohao.mmo.map.MapLogicServer;
 import com.iohao.mmo.person.PersonLogicServer;
 import lombok.extern.slf4j.Slf4j;
@@ -51,6 +54,8 @@ public class OneApplication {
     public static void main(String[] args) {
         SpringApplication.run(OneApplication.class, args);
 
+        extractedConfig();
+
         // 游戏逻辑服列表
         List<AbstractBrokerClientStartup> logicServers = listLogic();
 
@@ -73,30 +78,38 @@ public class OneApplication {
     }
 
     private static List<AbstractBrokerClientStartup> listLogic() {
+        // 登录
         LoginLogicServer loginLogicServer = new LoginLogicServer();
+        // 人物
         PersonLogicServer personLogicServer = new PersonLogicServer();
+        // 地图
         MapLogicServer mapLogicServer = new MapLogicServer();
+        // 等级
         LevelLogicServer levelLogicServer = new LevelLogicServer();
+        // 背包
         BagLogicServer bagLogicServer = new BagLogicServer();
+        // 邮件
+        MailLogicServer mailLogicServer = new MailLogicServer();
+        //装备
         EquipLogicServer equipLogicServer = new EquipLogicServer();
 
         // 游戏逻辑服列表
         return List.of(
-                // 登录
                 loginLogicServer
-                // 人物
                 , personLogicServer
-                // 地图
                 , mapLogicServer
-                // 等级
                 , levelLogicServer
-                // 背包
                 , bagLogicServer
-                // 装备
+                , mailLogicServer
                 , equipLogicServer
         );
     }
 
+    private static void extractedConfig() {
+        // 使用自定义 UserProcessor 构建 Executor 的策略
+        IoGameGlobalConfig.userProcessorExecutorStrategy = new MyUserProcessorExecutorStrategy();
+    }
+
     @Bean
     public ActionFactoryBeanForSpring actionFactoryBean() {
         // 将业务框架交给 spring 管理

+ 16 - 8
one-client/src/main/java/com/iohao/mmo/client/CommonClient.java

@@ -24,9 +24,11 @@ import com.iohao.game.external.client.user.ClientUser;
 import com.iohao.game.external.client.user.DefaultClientUser;
 import com.iohao.mmo.bag.client.BagInputCommandRegion;
 import com.iohao.mmo.bag.client.ItemInputCommandRegion;
+import com.iohao.mmo.common.provide.client.CommonInputCommandRegion;
 import com.iohao.mmo.equip.client.EquipInputCommandRegion;
 import com.iohao.mmo.level.client.LevelInputCommandRegion;
 import com.iohao.mmo.login.client.LoginInputCommandRegion;
+import com.iohao.mmo.mail.client.MailInputCommandRegion;
 import com.iohao.mmo.map.client.MapInputCommandRegion;
 import com.iohao.mmo.person.client.PersonInputCommandRegion;
 
@@ -52,29 +54,35 @@ public class CommonClient {
     }
 
     private static List<InputCommandRegion> listInputCommandRegion() {
+        // 登录
         LoginInputCommandRegion loginInputCommandRegion = new LoginInputCommandRegion();
+        // 通用的
+        CommonInputCommandRegion commonInputCommandRegion = new CommonInputCommandRegion();
+        // 地图
         MapInputCommandRegion mapInputCommandRegion = new MapInputCommandRegion();
+        // 人物、英雄
         PersonInputCommandRegion personInputCommandRegion = new PersonInputCommandRegion();
+        // 等级相关
         LevelInputCommandRegion levelInputCommandRegion = new LevelInputCommandRegion();
+        // 背包
         BagInputCommandRegion bagInputCommandRegion = new BagInputCommandRegion();
+        // 物品
         ItemInputCommandRegion itemInputCommandRegion = new ItemInputCommandRegion();
+        // 邮件
+        MailInputCommandRegion mailInputCommandRegion = new MailInputCommandRegion();
+        //装备
         EquipInputCommandRegion equipInputCommandRegion = new EquipInputCommandRegion();
 
         // 模拟请求数据
         return List.of(
-                // 登录
                 loginInputCommandRegion
-                // 地图
+                , commonInputCommandRegion
 //                , mapInputCommandRegion
-                // 人物、英雄
 //                , personInputCommandRegion
-                // 等级相关
-                , levelInputCommandRegion
-                // 物品
+//                , levelInputCommandRegion
                 , itemInputCommandRegion
-                // 背包
                 , bagInputCommandRegion
-                // 装备相关
+                , mailInputCommandRegion
                 , equipInputCommandRegion
         );
     }

+ 4 - 2
pom.xml

@@ -41,6 +41,8 @@
         <module>provide/level-provide</module>
         <module>logic/bag-logic</module>
         <module>provide/bag-provide</module>
+        <module>logic/mail-logic</module>
+        <module>provide/mail-provide</module>
         <module>logic/equip-logic</module>
         <module>provide/equip-provide</module>
 
@@ -56,7 +58,7 @@
         <encoding>UTF-8</encoding>
 
         <!-- 项目版本 -->
-        <ioGame.version>17.1.52</ioGame.version>
+        <ioGame.version>17.1.53</ioGame.version>
         <spring-boot.version>3.1.2</spring-boot.version>
         <!-- lombok 消除冗长的 Java 代码 https://mvnrepository.com/artifact/org.projectlombok/lombok -->
         <lombok.version>1.18.24</lombok.version>
@@ -170,4 +172,4 @@
         </plugins>
     </build>
 
-</project>
+</project>

+ 7 - 1
provide/all-provide/pom.xml

@@ -55,6 +55,12 @@
             <version>${project.parent.version}</version>
         </dependency>
 
+        <dependency>
+            <groupId>com.iohao.mmo</groupId>
+            <artifactId>mail-provide</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+
         <dependency>
             <groupId>com.iohao.mmo</groupId>
             <artifactId>equip-provide</artifactId>
@@ -62,4 +68,4 @@
         </dependency>
     </dependencies>
 
-</project>
+</project>

+ 8 - 12
provide/bag-provide/src/main/java/com/iohao/mmo/bag/client/BagExchange.java

@@ -21,12 +21,15 @@ package com.iohao.mmo.bag.client;
 import com.iohao.game.action.skeleton.core.CmdInfo;
 import com.iohao.game.action.skeleton.core.flow.FlowContext;
 import com.iohao.game.action.skeleton.protocol.ResponseMessage;
+import com.iohao.game.action.skeleton.protocol.wrapper.ByteValueList;
+import com.iohao.game.action.skeleton.protocol.wrapper.WrapperKit;
 import com.iohao.mmo.bag.cmd.BagCmd;
 import com.iohao.mmo.bag.proto.BagItemMessage;
-import com.iohao.mmo.common.kit.MessageKit;
 import com.iohao.mmo.common.provide.client.ExchangeKit;
 import lombok.experimental.UtilityClass;
 
+import java.util.List;
+
 /**
  * 背包模块,对外提供的访问 api
  *
@@ -35,14 +38,10 @@ import lombok.experimental.UtilityClass;
  */
 @UtilityClass
 public class BagExchange {
-    ResponseMessage incrementItemResponse(BagItemMessage bagItemMessage, FlowContext flowContext) {
+    public void incrementItems(List<BagItemMessage> bagItemMessageList, FlowContext flowContext) {
+        ByteValueList byteValueList = WrapperKit.ofListByteValue(bagItemMessageList);
         CmdInfo cmdInfo = BagCmd.of(BagCmd.incrementItem);
-        return ExchangeKit.invokeModuleMessage(flowContext, cmdInfo, bagItemMessage);
-    }
-
-    public BagItemMessage incrementItem(BagItemMessage bagItemMessage, FlowContext flowContext) {
-        ResponseMessage responseMessage = incrementItemResponse(bagItemMessage, flowContext);
-        return MessageKit.getData(responseMessage, BagItemMessage.class);
+        ExchangeKit.invokeModuleVoidMessage(flowContext, cmdInfo, byteValueList);
     }
 
     ResponseMessage decrementItemResponse(BagItemMessage bagItemMessage, FlowContext flowContext) {
@@ -50,8 +49,5 @@ public class BagExchange {
         return ExchangeKit.invokeModuleMessage(flowContext, cmdInfo, bagItemMessage);
     }
 
-    public BagItemMessage decrementItem(BagItemMessage bagItemMessage, FlowContext flowContext) {
-        ResponseMessage responseMessage = decrementItemResponse(bagItemMessage, flowContext);
-        return MessageKit.getData(responseMessage, BagItemMessage.class);
-    }
+
 }

+ 42 - 25
provide/bag-provide/src/main/java/com/iohao/mmo/bag/client/BagInputCommandRegion.java

@@ -20,19 +20,24 @@ package com.iohao.mmo.bag.client;
 
 import com.alibaba.fastjson2.JSONObject;
 import com.iohao.game.action.skeleton.protocol.wrapper.BoolValue;
+import com.iohao.game.action.skeleton.protocol.wrapper.ByteValueList;
+import com.iohao.game.action.skeleton.protocol.wrapper.WrapperKit;
 import com.iohao.game.external.client.AbstractInputCommandRegion;
 import com.iohao.game.external.client.command.InputRequestData;
 import com.iohao.game.external.client.kit.ScannerKit;
 import com.iohao.game.external.client.kit.SplitParam;
 import com.iohao.mmo.bag.ItemIdConst;
-import com.iohao.mmo.bag.client.ext.ClientBagAttr;
-import com.iohao.mmo.bag.client.ext.ItemMessageMap;
 import com.iohao.mmo.bag.cmd.BagCmd;
-import com.iohao.mmo.bag.proto.*;
+import com.iohao.mmo.bag.proto.BagItemMessage;
+import com.iohao.mmo.bag.proto.BagMessage;
+import com.iohao.mmo.bag.proto.UseItemMessage;
+import com.iohao.mmo.bag.proto.UseMessage;
+import com.iohao.mmo.common.provide.kit.ItemKit;
 import com.iohao.mmo.common.provide.kit.JsonKit;
 import com.iohao.mmo.common.snow.SnowKit;
 import lombok.extern.slf4j.Slf4j;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -44,48 +49,68 @@ public class BagInputCommandRegion extends AbstractInputCommandRegion {
     @Override
     public void initInputCommand() {
         this.inputCommandCreate.cmd = BagCmd.cmd;
+        this.inputCommandCreate.cmdName = "背包模块";
+
         request();
         useRequest();
+
+        listenBroadcast(ByteValueList.class, result -> {
+            List<BagItemMessage> list = result.toList(BagItemMessage.class);
+            log.info("物品变更 : {}", list.size());
+        }, BagCmd.broadcastChangeItems, "接收广播-物品变更");
     }
 
     @Override
     public void loginSuccessCallback() {
+
+        if (true) {
+            return;
+        }
+
+        List<BagItemMessage> list = new ArrayList<>();
         // 添加一些经验值道具
         BagItemMessage bagItemMessage = BagInternalHelper.ofBagItemMessage(ItemIdConst.expId);
         bagItemMessage.quantity = 10;
+        list.add(bagItemMessage);
         log.info("添加 {} 个经验值道具 {}", bagItemMessage.quantity, bagItemMessage);
-        ofRequestCommand(BagCmd.incrementItem).request(bagItemMessage);
 
         // 添加一些装备制造书材料
         bagItemMessage = BagInternalHelper.ofBagItemMessage(ItemIdConst.equipWeaponBook10);
         bagItemMessage.quantity = 1;
+        list.add(bagItemMessage);
         log.info("添加 {} 【装备-武器】制造书材料 {}", bagItemMessage.quantity, bagItemMessage);
-        ofRequestCommand(BagCmd.incrementItem).request(bagItemMessage);
 
         // 添加一些装备制造书材料
         bagItemMessage = BagInternalHelper.ofBagItemMessage(ItemIdConst.iron10);
         bagItemMessage.quantity = 1;
+        list.add(bagItemMessage);
         log.info("添加 {} 装备-制造材料-铁 {}", bagItemMessage.quantity, bagItemMessage);
-        ofRequestCommand(BagCmd.incrementItem).request(bagItemMessage);
+
+        ofRequestCommand(BagCmd.incrementItem).request(WrapperKit.ofListByteValue(list));
     }
 
     private void request() {
         ofCommand(BagCmd.bag).callback(BagMessage.class, result -> {
-            ItemMessageMap itemMessageMap = clientUser.option(ClientBagAttr.itemMessageMapAttrOption);
 
             BagMessage value = result.getValue();
 
             List<JSONObject> list = value.itemMap.values().stream().map(bagItemMessage -> {
                 String itemId = bagItemMessage.itemId;
-                JSONObject bagItemMessageJson = JSONObject.from(bagItemMessage);
-                JSONObject itemMessageJson = itemMessageMap.getItemMessageJSON(itemId);
+                JSONObject bagItemMessageJson = JsonKit.toJSON(bagItemMessage);
+                JSONObject itemMessageJson = ItemKit.toJSON(itemId);
                 return JsonKit.merge(bagItemMessageJson, itemMessageJson);
             }).toList();
 
             log.info("查询玩家背包 {}", JsonKit.toJsonString(list));
         }).setDescription("查询玩家背包");
 
-        InputRequestData inputRequestData = () -> {
+
+        ofCommand(BagCmd.incrementItem).callback(BagItemMessage.class, result -> {
+            var value = result.getValue();
+            log.info("value : {}", value);
+            // 重新查询一次背包
+            ofRequestCommand(BagCmd.bag).request();
+        }).setDescription("往背包添加(增加)物品").setInputRequestData(() -> {
             ScannerKit.log(() -> log.info("输入【1】表示添加一个可叠加的物品。"));
             String inputType = ScannerKit.nextLine("1");
 
@@ -103,17 +128,17 @@ public class BagInputCommandRegion extends AbstractInputCommandRegion {
 
             ScannerKit.log(() -> log.info("{}", bagItemMessage));
 
-            return bagItemMessage;
-        };
+            // 将请求参数包装成 list
+            return WrapperKit.ofListByteValue(List.of(bagItemMessage));
+        });
 
-        ofCommand(BagCmd.incrementItem).callback(BagItemMessage.class, result -> {
+        ofCommand(BagCmd.decrementItem).callback(BagItemMessage.class, result -> {
             var value = result.getValue();
             log.info("value : {}", value);
+
             // 重新查询一次背包
             ofRequestCommand(BagCmd.bag).request();
-        }).setDescription("往背包添加(增加)物品").setInputRequestData(inputRequestData);
-
-        inputRequestData = () -> {
+        }).setDescription("从背包减少物品").setInputRequestData(() -> {
             ScannerKit.log(() -> log.info("输入需要减少的物品信息,格式 [背包物品id-数量]"));
             String inputType = ScannerKit.nextLine("1-1");
 
@@ -128,15 +153,7 @@ public class BagInputCommandRegion extends AbstractInputCommandRegion {
             ScannerKit.log(() -> log.info("{}", bagItemMessage));
 
             return bagItemMessage;
-        };
-
-        ofCommand(BagCmd.decrementItem).callback(BagItemMessage.class, result -> {
-            var value = result.getValue();
-            log.info("value : {}", value);
-
-            // 重新查询一次背包
-            ofRequestCommand(BagCmd.bag).request();
-        }).setDescription("从背包减少物品").setInputRequestData(inputRequestData);
+        });
     }
 
     private void useRequest() {

+ 3 - 7
provide/bag-provide/src/main/java/com/iohao/mmo/bag/client/ItemInputCommandRegion.java

@@ -19,19 +19,17 @@
 package com.iohao.mmo.bag.client;
 
 import com.iohao.game.action.skeleton.protocol.wrapper.ByteValueList;
-import com.iohao.game.common.kit.attr.AttrOption;
 import com.iohao.game.external.client.AbstractInputCommandRegion;
-import com.iohao.mmo.bag.client.ext.ClientBagAttr;
-import com.iohao.mmo.bag.client.ext.ItemMessageMap;
 import com.iohao.mmo.bag.cmd.ItemCmd;
 import com.iohao.mmo.bag.proto.ItemMessage;
+import com.iohao.mmo.common.provide.kit.ItemKit;
 import lombok.extern.slf4j.Slf4j;
 
 import java.util.List;
 
 /**
  * @author 渔民小镇
- * @date 2023-08-13
+ * @date 2023-08-15
  */
 @Slf4j
 public class ItemInputCommandRegion extends AbstractInputCommandRegion {
@@ -41,9 +39,7 @@ public class ItemInputCommandRegion extends AbstractInputCommandRegion {
 
         ofCommand(ItemCmd.listItem).callback(ByteValueList.class, result -> {
             List<ItemMessage> list = result.toList(ItemMessage.class);
-            AttrOption<ItemMessageMap> itemMessageMapAttrOption = ClientBagAttr.itemMessageMapAttrOption;
-            ItemMessageMap itemMessageMap = new ItemMessageMap(list);
-            clientUser.option(itemMessageMapAttrOption, itemMessageMap);
+            list.forEach(itemMessage -> ItemKit.itemNodeMap.add(itemMessage.itemId, itemMessage.name, itemMessage.description));
         }).setDescription("物品配置列表");
     }
 

+ 3 - 1
provide/bag-provide/src/main/java/com/iohao/mmo/bag/cmd/BagCmd.java

@@ -35,12 +35,14 @@ public interface BagCmd {
     int incrementItem = 2;
     /** 减少物品 */
     int decrementItem = 3;
-
     /** 使用物品 */
     int use = 5;
     /** 打造装备 */
     int useBuildEquip = 6;
 
+    /** 推送物品变更 */
+    int broadcastChangeItems = 50;
+
     static CmdInfo of(int subCmd) {
         return CmdInfo.of(cmd, subCmd);
     }

+ 1 - 1
provide/bag-provide/src/main/java/com/iohao/mmo/bag/proto/ItemMessage.java

@@ -27,7 +27,7 @@ import lombok.experimental.FieldDefaults;
  * 物品信息
  *
  * @author 渔民小镇
- * @date 2023-08-13
+ * @date 2023-08-15
  */
 @ToString
 @ProtobufClass

+ 7 - 0
provide/common-provide/pom.xml

@@ -44,5 +44,12 @@
             <artifactId>common-core</artifactId>
             <version>${project.parent.version}</version>
         </dependency>
+
+        <!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct -->
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct</artifactId>
+            <version>${org.mapstruct.version}</version>
+        </dependency>
     </dependencies>
 </project>

+ 63 - 0
provide/common-provide/src/main/java/com/iohao/mmo/common/provide/client/CommonExchange.java

@@ -0,0 +1,63 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.common.provide.client;
+
+import com.iohao.game.action.skeleton.core.CmdInfo;
+import com.iohao.game.action.skeleton.core.commumication.BroadcastContext;
+import com.iohao.game.action.skeleton.core.flow.FlowContext;
+import com.iohao.game.action.skeleton.protocol.wrapper.ByteValueList;
+import com.iohao.game.action.skeleton.protocol.wrapper.WrapperKit;
+import com.iohao.game.bolt.broker.core.client.BrokerClientHelper;
+import com.iohao.game.common.kit.CollKit;
+import com.iohao.mmo.common.core.flow.MyFlowContext;
+import com.iohao.mmo.common.provide.cmd.CommonCmd;
+import com.iohao.mmo.common.provide.proto.ShowItemMessage;
+import lombok.experimental.UtilityClass;
+
+import java.util.List;
+
+/**
+ * 用于界面显示的物品消息,类似单次跑马灯
+ *
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@UtilityClass
+public class CommonExchange {
+    public void broadcastShowItem(List<ShowItemMessage> itemMessages, long userId) {
+        CmdInfo cmdInfo = CommonCmd.of(CommonCmd.broadcastShowItem);
+        ByteValueList byteValueList = WrapperKit.ofListByteValue(itemMessages);
+
+        BroadcastContext broadcastContext = BrokerClientHelper.getBroadcastContext();
+        broadcastContext.broadcast(cmdInfo, byteValueList, userId);
+    }
+
+    public void broadcastShowItem(List<ShowItemMessage> itemMessages, FlowContext flowContext) {
+        if (CollKit.isEmpty(itemMessages)) {
+            return;
+        }
+
+        CmdInfo cmdInfo = CommonCmd.of(CommonCmd.broadcastShowItem);
+        ByteValueList byteValueList = WrapperKit.ofListByteValue(itemMessages);
+
+        if (flowContext instanceof MyFlowContext myFlowContext) {
+            myFlowContext.broadcast(cmdInfo, byteValueList);
+        }
+    }
+}

+ 57 - 0
provide/common-provide/src/main/java/com/iohao/mmo/common/provide/client/CommonInputCommandRegion.java

@@ -0,0 +1,57 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.common.provide.client;
+
+import com.iohao.game.action.skeleton.protocol.wrapper.ByteValueList;
+import com.iohao.game.external.client.AbstractInputCommandRegion;
+import com.iohao.mmo.common.provide.cmd.CommonCmd;
+import com.iohao.mmo.common.provide.kit.ItemKit;
+import com.iohao.mmo.common.provide.kit.ItemNode;
+import com.iohao.mmo.common.provide.proto.ShowItemMessage;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.stream.Collectors;
+
+/**
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@Slf4j
+public class CommonInputCommandRegion extends AbstractInputCommandRegion {
+    @Override
+    public void initInputCommand() {
+        this.inputCommandCreate.cmd = CommonCmd.cmd;
+
+        listenBroadcast(ByteValueList.class, result -> {
+
+            String collect = result.toList(ShowItemMessage.class)
+                    .stream()
+                    .map(this::convert)
+                    .map(ItemKit::toString)
+                    .collect(Collectors.joining());
+
+            log.info("获得新物品 {}", collect);
+
+        }, CommonCmd.broadcastShowItem, "获得新物品");
+    }
+
+    ItemNode convert(ShowItemMessage showItemMessage) {
+        return new ItemNode(showItemMessage.itemId, showItemMessage.quantity);
+    }
+}

+ 6 - 0
provide/common-provide/src/main/java/com/iohao/mmo/common/provide/client/ExchangeKit.java

@@ -25,6 +25,7 @@ import com.iohao.game.action.skeleton.core.flow.FlowContext;
 import com.iohao.game.action.skeleton.core.flow.attr.FlowAttr;
 import com.iohao.game.action.skeleton.protocol.RequestMessage;
 import com.iohao.game.action.skeleton.protocol.ResponseMessage;
+import com.iohao.game.bolt.broker.core.client.BrokerClientHelper;
 import lombok.experimental.UtilityClass;
 
 /**
@@ -56,4 +57,9 @@ public class ExchangeKit {
         InvokeModuleContext invokeModuleContext = brokerClientContext.getInvokeModuleContext();
         invokeModuleContext.invokeModuleVoidMessage(requestMessage);
     }
+
+    public void invokeModuleVoidMessage(CmdInfo cmdInfo, Object data) {
+        InvokeModuleContext invokeModuleContext = BrokerClientHelper.getInvokeModuleContext();
+        invokeModuleContext.invokeModuleVoidMessage(cmdInfo, data);
+    }
 }

+ 5 - 1
provide/common-provide/src/main/java/com/iohao/mmo/common/provide/cmd/CmdModule.java

@@ -25,6 +25,8 @@ package com.iohao.mmo.common.provide.cmd;
  * @date 2023-07-21
  */
 public interface CmdModule {
+    /** 相对通用的模块 */
+    int commandCmd = 0;
     /** 大厅 - 登录 */
     int loginCmd = 1;
     /** 人物 */
@@ -39,6 +41,8 @@ public interface CmdModule {
     int itemCmd = 6;
     /** 背包 */
     int bagCmd = 7;
+    /** 邮件 */
+    int mailCmd = 8;
     /** 装备 */
-    int equipCmd = 8;
+    int equipCmd = 10;
 }

+ 34 - 0
provide/common-provide/src/main/java/com/iohao/mmo/common/provide/cmd/CommonCmd.java

@@ -0,0 +1,34 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.common.provide.cmd;
+
+import com.iohao.game.action.skeleton.core.CmdInfo;
+
+/**
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+public interface CommonCmd {
+    int cmd = CmdModule.commandCmd;
+    int broadcastShowItem = 100;
+
+    static CmdInfo of(int subCmd) {
+        return CmdInfo.of(cmd, subCmd);
+    }
+}

+ 49 - 0
provide/common-provide/src/main/java/com/iohao/mmo/common/provide/kit/ItemKit.java

@@ -0,0 +1,49 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.common.provide.kit;
+
+import com.alibaba.fastjson2.JSONObject;
+import lombok.experimental.UtilityClass;
+
+/**
+ * 客户端 - 物品通用工具
+ *
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@UtilityClass
+public class ItemKit {
+    public ItemNodeMap itemNodeMap = new ItemNodeMap();
+
+    public String toString(ItemNode itemNode) {
+        var node = itemNodeMap.getItem(itemNode.itemId());
+        String line = "\n物品信息:[%s x %s];物品描述:%s";
+
+        return String.format(line
+                , node.name
+                , itemNode.quantity()
+                , node.description
+        );
+    }
+
+    public JSONObject toJSON(String itemId) {
+        ItemNodeMap.TheItemInfo item = itemNodeMap.getItem(itemId);
+        return JsonKit.toJSON(item);
+    }
+}

+ 28 - 0
provide/common-provide/src/main/java/com/iohao/mmo/common/provide/kit/ItemNode.java

@@ -0,0 +1,28 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.common.provide.kit;
+
+/**
+ * @param itemId   物品 id
+ * @param quantity 物品数量
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+public record ItemNode(String itemId, int quantity) {
+}

+ 63 - 0
provide/common-provide/src/main/java/com/iohao/mmo/common/provide/kit/ItemNodeMap.java

@@ -0,0 +1,63 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.common.provide.kit;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.FieldDefaults;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@Getter
+@Setter
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class ItemNodeMap {
+    final Map<String, TheItemInfo> map = new HashMap<>();
+
+    public void add(String itemId, String name, String description) {
+        TheItemInfo theItemInfo = new TheItemInfo(itemId, name, description);
+
+        map.put(itemId, theItemInfo);
+    }
+
+    public TheItemInfo getItem(String itemId) {
+        return map.get(itemId);
+    }
+
+    static class TheItemInfo {
+        /** itemId */
+        String itemId;
+        /** 物品名 */
+        String name;
+        /** 物品描述 */
+        String description;
+
+        public TheItemInfo(String itemId, String name, String description) {
+            this.itemId = itemId;
+            this.name = name;
+            this.description = description;
+        }
+    }
+}

+ 4 - 0
provide/common-provide/src/main/java/com/iohao/mmo/common/provide/kit/JsonKit.java

@@ -41,6 +41,10 @@ public class JsonKit {
         return JSON.toJSONString(value, JSONWriter.Feature.PrettyFormat);
     }
 
+    public JSONObject toJSON(Object value) {
+        return JSONObject.from(value);
+    }
+
     public JSONObject merge(JSONObject... jsonObjects) {
         JSONObject json = new JSONObject();
 

+ 11 - 23
provide/bag-provide/src/main/java/com/iohao/mmo/bag/client/ext/ItemMessageMap.java → provide/common-provide/src/main/java/com/iohao/mmo/common/provide/proto/ShowItemMessage.java

@@ -16,37 +16,25 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
-package com.iohao.mmo.bag.client.ext;
+package com.iohao.mmo.common.provide.proto;
 
-import com.alibaba.fastjson2.JSONObject;
-import com.iohao.mmo.bag.proto.ItemMessage;
+import com.baidu.bjf.remoting.protobuf.annotation.ProtobufClass;
 import lombok.AccessLevel;
 import lombok.ToString;
 import lombok.experimental.FieldDefaults;
 
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
 /**
+ * 显示的物品消息
+ *
  * @author 渔民小镇
- * @date 2023-08-13
+ * @date 2023-08-15
  */
 @ToString
+@ProtobufClass
 @FieldDefaults(level = AccessLevel.PUBLIC)
-public class ItemMessageMap {
-    final Map<String, ItemMessage> map = new HashMap<>();
-
-    public ItemMessageMap(List<ItemMessage> itemMessageList) {
-        itemMessageList.forEach(itemMessage -> map.put(itemMessage.itemId, itemMessage));
-    }
-
-    public ItemMessage getItemMessage(String itemId) {
-        return map.get(itemId);
-    }
-
-    public JSONObject getItemMessageJSON(String itemId) {
-        ItemMessage itemMessage = getItemMessage(itemId);
-        return JSONObject.from(itemMessage);
-    }
+public class ShowItemMessage {
+    /** 物品 id */
+    String itemId;
+    /** 物品数量 */
+    int quantity;
 }

+ 2 - 2
provide/level-provide/src/main/java/com/iohao/mmo/level/client/LevelInputCommandRegion.java

@@ -35,10 +35,10 @@ public class LevelInputCommandRegion extends AbstractInputCommandRegion {
     @Override
     public void initInputCommand() {
         this.inputCommandCreate.cmd = LevelCmd.cmd;
-        request();
+        this.inputCommandCreate.cmdName = "等级模块";
 
+        request();
         listen();
-
     }
 
     private void listen() {

+ 38 - 0
provide/mail-provide/.gitignore

@@ -0,0 +1,38 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store

+ 33 - 0
provide/mail-provide/pom.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.iohao.mmo</groupId>
+        <artifactId>game</artifactId>
+        <version>1.0-SNAPSHOT</version>
+        <relativePath>../../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>mail-provide</artifactId>
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>com.iohao.mmo</groupId>
+            <artifactId>common-provide</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.iohao.mmo</groupId>
+            <artifactId>bag-provide</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 57 - 0
provide/mail-provide/src/main/java/com/iohao/mmo/mail/client/MailExchange.java

@@ -0,0 +1,57 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.mail.client;
+
+import com.iohao.game.action.skeleton.core.CmdInfo;
+import com.iohao.game.action.skeleton.protocol.wrapper.ByteValueList;
+import com.iohao.game.action.skeleton.protocol.wrapper.WrapperKit;
+import com.iohao.game.common.kit.CollKit;
+import com.iohao.mmo.common.provide.client.ExchangeKit;
+import com.iohao.mmo.mail.cmd.MailCmd;
+import com.iohao.mmo.mail.proto.InternalMailMessage;
+import lombok.experimental.UtilityClass;
+
+import java.util.List;
+
+/**
+ * 邮件模块,对外提供的访问 api
+ * <pre>
+ *     应用场景:后台、触发某个系统的活动奖励
+ * </pre>
+ *
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@UtilityClass
+public class MailExchange {
+    public void addEmail(InternalMailMessage internalMailMessage) {
+        addEmail(List.of(internalMailMessage));
+    }
+
+    public void addEmail(List<InternalMailMessage> internalMailMessages) {
+
+        if (CollKit.isEmpty(internalMailMessages)) {
+            return;
+        }
+
+        CmdInfo cmdInfo = MailCmd.of(MailCmd.addMail);
+        ByteValueList byteValueList = WrapperKit.ofListByteValue(internalMailMessages);
+        ExchangeKit.invokeModuleVoidMessage(cmdInfo, byteValueList);
+    }
+}

+ 154 - 0
provide/mail-provide/src/main/java/com/iohao/mmo/mail/client/MailInputCommandRegion.java

@@ -0,0 +1,154 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.mail.client;
+
+import com.iohao.game.action.skeleton.protocol.wrapper.BoolValue;
+import com.iohao.game.action.skeleton.protocol.wrapper.ByteValueList;
+import com.iohao.game.action.skeleton.protocol.wrapper.StringValue;
+import com.iohao.game.action.skeleton.protocol.wrapper.WrapperKit;
+import com.iohao.game.common.kit.TimeKit;
+import com.iohao.game.external.client.AbstractInputCommandRegion;
+import com.iohao.game.external.client.kit.ScannerKit;
+import com.iohao.mmo.bag.ItemIdConst;
+import com.iohao.mmo.common.provide.kit.ItemKit;
+import com.iohao.mmo.common.provide.kit.ItemNode;
+import com.iohao.mmo.mail.cmd.MailCmd;
+import com.iohao.mmo.mail.kit.InternalMailBuilder;
+import com.iohao.mmo.mail.proto.InternalMailMessage;
+import com.iohao.mmo.mail.proto.MailAttachmentMessage;
+import com.iohao.mmo.mail.proto.MailMessage;
+import com.iohao.mmo.mail.proto.MailStatusMessageEnum;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@Slf4j
+public class MailInputCommandRegion extends AbstractInputCommandRegion {
+    @Override
+    public void initInputCommand() {
+        this.inputCommandCreate.cmd = MailCmd.cmd;
+        this.inputCommandCreate.cmdName = "邮件";
+
+        request();
+        listen();
+    }
+
+    private void request() {
+        ofCommand(MailCmd.listMail).callback(ByteValueList.class, result -> {
+            var list = result.toList(MailMessage.class);
+            log.info("查看玩家邮件列表 - 总数: {}", list.size());
+            list.stream()
+                    .map(MailToString::new)
+                    .forEach(mailToString -> {
+                        log.info(mailToString.format, mailToString.arguments);
+                        System.out.println("-------------------------------------------------------");
+                    });
+        }).setDescription("查看玩家邮件列表");
+
+        ofCommand(MailCmd.addMail).setDescription("内部 action - 给玩家奖励一个【系统邮件】").setInputRequestData(() -> {
+            String body = """
+                    玩家编号[%s],系统感觉你今天很弱鸡,特意送你一些物品
+                    1.经验值道具 x 2
+                    2.武器制作书-10级 x 1
+                    3.打造精铁-10级 x 1
+                    """;
+
+            InternalMailBuilder internalMailBuilder = InternalMailBuilder.newSystemMailBuilder(String.format(body, userId));
+            internalMailBuilder.addMailAttachment(ItemIdConst.expId, 2)
+                    .addMailAttachment(ItemIdConst.equipWeaponBook10, 1)
+                    .addMailAttachment(ItemIdConst.iron10, 1);
+
+            InternalMailMessage internalMailMessage = internalMailBuilder.build(userId);
+            return WrapperKit.ofListByteValue(List.of(internalMailMessage));
+        });
+
+        ofCommand(MailCmd.deleteMail).callback(BoolValue.class, result -> {
+            BoolValue value = result.getValue();
+            log.info("删除{}", value.value ? "成功" : "失败");
+        }).setDescription("删除指定邮件").setInputRequestData(() -> {
+            ScannerKit.log(() -> log.info("输入需要删除的邮件 id"));
+            String inputType = ScannerKit.nextLine("1");
+            return StringValue.of(inputType);
+        });
+
+        ofCommand(MailCmd.deleteMails).callback(BoolValue.class, result -> {
+            BoolValue value = result.getValue();
+            log.info("删除{}", value.value ? "成功" : "失败");
+        }).setDescription("一键删除所有已开封和过期的邮件");
+
+        ofCommand(MailCmd.openMail).setDescription("领取指定未开封的邮件").setInputRequestData(() -> {
+            ScannerKit.log(() -> log.info("输入需要领取的邮件 id"));
+            String inputType = ScannerKit.nextLine("1");
+            return StringValue.of(inputType);
+        });
+
+        ofCommand(MailCmd.openMails).setDescription("一键领取所有未开封的邮件");
+    }
+
+    private void listen() {
+        listenBroadcast(MailMessage.class, result -> {
+            MailMessage mailMessage = result.getValue();
+            log.info("-----接收新邮件-----");
+            MailToString mailToString = new MailToString(mailMessage);
+            log.info(mailToString.format, mailToString.arguments);
+        }, MailCmd.sendNewMail, "接收新邮件");
+    }
+
+    static class MailToString {
+        String format;
+        Object[] arguments;
+
+        public MailToString(MailMessage mailMessage) {
+            format = """
+                                        
+                    【邮件主题 {}】【邮件id {}】【邮件状态:{}】
+                    【发送时间:{} --- 过期时间:{}】                  
+                    邮件正文 : {}
+                    附件(奖励)列表:{}
+                    """;
+
+            String mailAttachmentStr = mailMessage.mailAttachments
+                    .stream()
+                    .map(this::convert)
+                    .map(ItemKit::toString)
+                    .collect(Collectors.joining());
+
+            List<Object> list = new ArrayList<>();
+            list.add(mailMessage.subject);
+            list.add(mailMessage.id);
+            list.add(mailMessage.mailStatus == MailStatusMessageEnum.SEAL ? "未领取" : "已经领取");
+            list.add(TimeKit.formatter(mailMessage.milliseconds));
+            list.add(TimeKit.formatter(mailMessage.expiredMilliseconds));
+            list.add(mailMessage.body);
+            list.add(mailAttachmentStr);
+
+            arguments = list.toArray();
+        }
+
+        ItemNode convert(MailAttachmentMessage attachmentMessage) {
+            return new ItemNode(attachmentMessage.itemId, attachmentMessage.quantity);
+        }
+    }
+}

+ 49 - 0
provide/mail-provide/src/main/java/com/iohao/mmo/mail/cmd/MailCmd.java

@@ -0,0 +1,49 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.mail.cmd;
+
+import com.iohao.game.action.skeleton.core.CmdInfo;
+import com.iohao.mmo.common.provide.cmd.CmdModule;
+
+/**
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+public interface MailCmd {
+    int cmd = CmdModule.mailCmd;
+    /** 查看玩家邮件列表 */
+    int listMail = 1;
+    /** 添加邮件 */
+    int addMail = 2;
+    /** 删除单个邮件-指定邮件 */
+    int deleteMail = 3;
+    /** 一键删除多个邮件,删除所有已开封和过期的邮件 */
+    int deleteMails = 4;
+    /** 打开单个邮件-指定邮件 */
+    int openMail = 5;
+    /** 打开多个邮件-所有未开封的邮件 */
+    int openMails = 6;
+
+    /** 新邮件 - 邮服务器推送 */
+    int sendNewMail = 100;
+
+    static CmdInfo of(int subCmd) {
+        return CmdInfo.of(cmd, subCmd);
+    }
+}

+ 143 - 0
provide/mail-provide/src/main/java/com/iohao/mmo/mail/kit/InternalMailBuilder.java

@@ -0,0 +1,143 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.mail.kit;
+
+import com.iohao.game.common.kit.TimeKit;
+import com.iohao.mmo.mail.proto.InternalMailMessage;
+import com.iohao.mmo.mail.proto.MailAttachmentMessage;
+import com.iohao.mmo.mail.proto.MailMessage;
+import com.iohao.mmo.mail.proto.MailStatusMessageEnum;
+import lombok.AccessLevel;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import lombok.experimental.FieldDefaults;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 邮件构建器
+ *
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@Setter
+@Accessors(chain = true)
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class InternalMailBuilder {
+    /** 邮件主题 */
+    String subject;
+    /** 邮件正文 */
+    String body;
+
+    /** 发件人 */
+    String senderName;
+    /** 发件人 userId */
+    Long senderUserId;
+    /** 邮件附件(奖励) */
+    @Setter(AccessLevel.PRIVATE)
+    List<MailAttachmentMessage> mailAttachments = new ArrayList<>();
+    /**
+     * 邮件过期时间,默认 30 后天过期
+     * <pre>
+     *     Duration.ofDays(2); 表示两天后过期
+     * </pre>
+     */
+    Duration duration = Duration.ofDays(30);
+
+    InternalMailBuilder() {
+    }
+
+    public static InternalMailBuilder newBuilder() {
+        return new InternalMailBuilder();
+    }
+
+    public static InternalMailBuilder newSystemMailBuilder(String body) {
+        InternalMailBuilder internalMailBuilder = new InternalMailBuilder();
+
+        internalMailBuilder.subject = "系统邮件";
+        internalMailBuilder.senderName = "系统";
+        internalMailBuilder.senderUserId = 0L;
+        internalMailBuilder.body = body;
+
+        return internalMailBuilder;
+    }
+
+    /**
+     * 添加奖励附件
+     *
+     * @param itemId   物品 id
+     * @param quantity 奖励数量
+     * @return MailBuilder
+     */
+    public InternalMailBuilder addMailAttachment(String itemId, int quantity) {
+
+        if (quantity < 0) {
+            throw new IllegalArgumentException();
+        }
+
+        Objects.requireNonNull(itemId);
+
+        MailAttachmentMessage attachmentMessage = new MailAttachmentMessage();
+        attachmentMessage.id = itemId;
+        attachmentMessage.itemId = itemId;
+        attachmentMessage.quantity = quantity;
+
+        mailAttachments.add(attachmentMessage);
+
+        return this;
+    }
+
+    /**
+     * 接收邮件的玩家
+     *
+     * @param userId userId
+     * @return InternalMailMessage
+     */
+    public InternalMailMessage build(long userId) {
+        Objects.requireNonNull(subject);
+        Objects.requireNonNull(body);
+        Objects.requireNonNull(senderName);
+        Objects.requireNonNull(senderUserId);
+        Objects.requireNonNull(duration);
+
+        if (userId <= 0) {
+            throw new IllegalArgumentException();
+        }
+
+        // 奖励邮件
+        MailMessage mailMessage = new MailMessage();
+        mailMessage.senderName = senderName;
+        mailMessage.senderUserId = senderUserId;
+        mailMessage.subject = subject;
+        mailMessage.body = body;
+        mailMessage.milliseconds = TimeKit.currentTimeMillis();
+        mailMessage.expiredMilliseconds = mailMessage.milliseconds + duration.toMillis();
+        mailMessage.mailAttachments = this.mailAttachments;
+        mailMessage.mailStatus = MailStatusMessageEnum.SEAL;
+
+        // 内部邮件
+        InternalMailMessage internalMailMessage = new InternalMailMessage();
+        internalMailMessage.userId = userId;
+        internalMailMessage.mailMessage = mailMessage;
+        return internalMailMessage;
+    }
+}

+ 40 - 0
provide/mail-provide/src/main/java/com/iohao/mmo/mail/proto/InternalMailMessage.java

@@ -0,0 +1,40 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.mail.proto;
+
+import com.baidu.bjf.remoting.protobuf.annotation.ProtobufClass;
+import lombok.AccessLevel;
+import lombok.ToString;
+import lombok.experimental.FieldDefaults;
+
+/**
+ * 内部邮件奖励
+ *
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@ToString
+@ProtobufClass
+@FieldDefaults(level = AccessLevel.PUBLIC)
+public class InternalMailMessage {
+    /** userId */
+    long userId;
+    /** 玩家邮件 */
+    MailMessage mailMessage;
+}

+ 41 - 0
provide/mail-provide/src/main/java/com/iohao/mmo/mail/proto/MailAttachmentMessage.java

@@ -0,0 +1,41 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.mail.proto;
+
+import com.baidu.bjf.remoting.protobuf.annotation.ProtobufClass;
+import lombok.AccessLevel;
+import lombok.ToString;
+import lombok.experimental.FieldDefaults;
+
+/**
+ * 邮件附件(奖励)
+ *
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@ToString
+@ProtobufClass
+@FieldDefaults(level = AccessLevel.PUBLIC)
+public class MailAttachmentMessage {
+    String id;
+    /** 物品 id */
+    String itemId;
+    /** 物品数量 */
+    int quantity;
+}

+ 65 - 0
provide/mail-provide/src/main/java/com/iohao/mmo/mail/proto/MailMessage.java

@@ -0,0 +1,65 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.mail.proto;
+
+import com.baidu.bjf.remoting.protobuf.annotation.ProtobufClass;
+import lombok.AccessLevel;
+import lombok.ToString;
+import lombok.experimental.FieldDefaults;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 玩家邮件
+ *
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@ToString
+@ProtobufClass
+@FieldDefaults(level = AccessLevel.PUBLIC)
+public class MailMessage {
+    String id;
+    /** 发件人 */
+    String senderName;
+    /** 发件人 userId */
+    long senderUserId;
+    /** 邮件主题 */
+    String subject;
+    /** 邮件正文 */
+    String body;
+    /** 发送时间 */
+    long milliseconds;
+    /** 过期时间 */
+    long expiredMilliseconds;
+    /** 邮件状态 */
+    MailStatusMessageEnum mailStatus;
+    /** 附件(奖励) */
+    List<MailAttachmentMessage> mailAttachments;
+
+    public void addMailAttachment(MailAttachmentMessage mailAttachment) {
+        if (Objects.isNull(mailAttachments)) {
+            this.mailAttachments = new ArrayList<>();
+        }
+
+        this.mailAttachments.add(mailAttachment);
+    }
+}

+ 33 - 0
provide/mail-provide/src/main/java/com/iohao/mmo/mail/proto/MailStatusMessageEnum.java

@@ -0,0 +1,33 @@
+/*
+ * ioGame
+ * Copyright (C) 2021 - 2023  渔民小镇 (262610965@qq.com、luoyizhu@gmail.com) . All Rights Reserved.
+ * # iohao.com . 渔民小镇
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.iohao.mmo.mail.proto;
+
+import com.baidu.bjf.remoting.protobuf.annotation.ProtobufClass;
+
+/**
+ * @author 渔民小镇
+ * @date 2023-08-15
+ */
+@ProtobufClass
+public enum MailStatusMessageEnum {
+    /** 密封的邮件、未开封的邮件 */
+    SEAL,
+    /** 已开封的邮件、奖励已经被领取了 */
+    OPEN;
+}