Kaynağa Gözat

上传 cms 和 contract 服务源码

Co-authored-by: Cursor <cursoragent@cursor.com>
jack 1 ay önce
ebeveyn
işleme
dde98cb215
66 değiştirilmiş dosya ile 2644 ekleme ve 0 silme
  1. 6 0
      cms/.gitignore
  2. 93 0
      cms/README.md
  3. 76 0
      cms/cms-api/pom.xml
  4. 14 0
      cms/cms-api/src/main/java/com/cms/CmsApplication.java
  5. 35 0
      cms/cms-api/src/main/java/com/cms/config/WebMvcConfig.java
  6. 81 0
      cms/cms-api/src/main/java/com/cms/controller/HelpController.java
  7. 33 0
      cms/cms-api/src/main/java/com/cms/interceptor/AuthenticationInterceptor.java
  8. 9 0
      cms/cms-api/src/main/java/com/cms/mapper/SysHelpCategoryMapper.java
  9. 9 0
      cms/cms-api/src/main/java/com/cms/mapper/SysHelpMapper.java
  10. 41 0
      cms/cms-api/src/main/java/com/cms/service/SysHelpCategoryService.java
  11. 58 0
      cms/cms-api/src/main/java/com/cms/service/SysHelpService.java
  12. 28 0
      cms/cms-api/src/main/resources/application.yml
  13. 88 0
      cms/cms-api/src/main/resources/log4j2-spring.xml
  14. 28 0
      cms/cms-entity/pom.xml
  15. 49 0
      cms/cms-entity/src/main/java/com/cms/entity/SysHelp.java
  16. 47 0
      cms/cms-entity/src/main/java/com/cms/entity/SysHelpCategory.java
  17. 69 0
      cms/cms-entity/src/main/java/com/cms/entity/resp/MessageResult.java
  18. 16 0
      cms/cms-entity/src/main/java/com/cms/entity/transform/AuthMember.java
  19. 116 0
      cms/pom.xml
  20. 3 0
      contract/.gitignore
  21. 93 0
      contract/README.md
  22. 80 0
      contract/contract-api/pom.xml
  23. 12 0
      contract/contract-api/src/main/java/com/contract/ContractApplication.java
  24. 54 0
      contract/contract-api/src/main/java/com/contract/config/WebMvcConfig.java
  25. 57 0
      contract/contract-api/src/main/java/com/contract/controller/AppBannerController.java
  26. 57 0
      contract/contract-api/src/main/java/com/contract/controller/AppHeaderController.java
  27. 28 0
      contract/contract-api/src/main/java/com/contract/controller/CoinExtController.java
  28. 29 0
      contract/contract-api/src/main/java/com/contract/controller/CurrencyCoinController.java
  29. 28 0
      contract/contract-api/src/main/java/com/contract/controller/NodeInfoController.java
  30. 52 0
      contract/contract-api/src/main/java/com/contract/controller/UserWatchlistController.java
  31. 33 0
      contract/contract-api/src/main/java/com/contract/interceptor/AuthenticationInterceptor.java
  32. 18 0
      contract/contract-api/src/main/java/com/contract/mapper/AppBannerMapper.java
  33. 18 0
      contract/contract-api/src/main/java/com/contract/mapper/AppHeaderMapper.java
  34. 9 0
      contract/contract-api/src/main/java/com/contract/mapper/CoinExtMapper.java
  35. 9 0
      contract/contract-api/src/main/java/com/contract/mapper/CurrencyCoinMapper.java
  36. 9 0
      contract/contract-api/src/main/java/com/contract/mapper/NodeInfoMapper.java
  37. 9 0
      contract/contract-api/src/main/java/com/contract/mapper/UserWatchlistMapper.java
  38. 14 0
      contract/contract-api/src/main/java/com/contract/service/AppBannerService.java
  39. 14 0
      contract/contract-api/src/main/java/com/contract/service/AppHeaderService.java
  40. 9 0
      contract/contract-api/src/main/java/com/contract/service/CoinExtService.java
  41. 8 0
      contract/contract-api/src/main/java/com/contract/service/CurrencyCoinService.java
  42. 11 0
      contract/contract-api/src/main/java/com/contract/service/NodeInfoService.java
  43. 15 0
      contract/contract-api/src/main/java/com/contract/service/UserWatchlistService.java
  44. 35 0
      contract/contract-api/src/main/java/com/contract/service/impl/AppBannerServiceImpl.java
  45. 35 0
      contract/contract-api/src/main/java/com/contract/service/impl/AppHeaderServiceImpl.java
  46. 19 0
      contract/contract-api/src/main/java/com/contract/service/impl/CoinExtServiceImpl.java
  47. 13 0
      contract/contract-api/src/main/java/com/contract/service/impl/CurrencyCoinServiceImpl.java
  48. 21 0
      contract/contract-api/src/main/java/com/contract/service/impl/NodeInfoServiceImpl.java
  49. 59 0
      contract/contract-api/src/main/java/com/contract/service/impl/UserWatchlistServiceImpl.java
  50. 25 0
      contract/contract-api/src/main/java/com/contract/util/RequestUtils.java
  51. 28 0
      contract/contract-api/src/main/resources/application.yml
  52. 88 0
      contract/contract-api/src/main/resources/log4j2-spring.xml
  53. 5 0
      contract/contract-api/src/main/resources/mapper/UserWatchlistMapper.xml
  54. 28 0
      contract/contract-entity/pom.xml
  55. 49 0
      contract/contract-entity/src/main/java/com/contract/entity/AppBanner.java
  56. 61 0
      contract/contract-entity/src/main/java/com/contract/entity/AppHeader.java
  57. 56 0
      contract/contract-entity/src/main/java/com/contract/entity/CoinExt.java
  58. 36 0
      contract/contract-entity/src/main/java/com/contract/entity/CurrencyCoin.java
  59. 33 0
      contract/contract-entity/src/main/java/com/contract/entity/NodeInfo.java
  60. 26 0
      contract/contract-entity/src/main/java/com/contract/entity/UserWatchlist.java
  61. 115 0
      contract/contract-entity/src/main/java/com/contract/entity/resp/MessageResult.java
  62. 16 0
      contract/contract-entity/src/main/java/com/contract/entity/transform/AuthMember.java
  63. 22 0
      contract/contract-entity/src/main/java/com/contract/enums/I18nLangKeys.java
  64. 55 0
      contract/contract-entity/src/main/java/com/contract/enums/MessageCode.java
  65. 149 0
      contract/doc/sys_help_category_design.md
  66. 105 0
      contract/pom.xml

+ 6 - 0
cms/.gitignore

@@ -0,0 +1,6 @@
+# IDE
+.idea/
+
+# Build output
+target/
+*.class

+ 93 - 0
cms/README.md

@@ -0,0 +1,93 @@
+# cms
+
+
+
+## Getting started
+
+To make it easy for you to get started with GitLab, here's a list of recommended next steps.
+
+Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
+
+## Add your files
+
+* [Create](https://docs.gitlab.com/user/project/repository/web_editor/#create-a-file) or [upload](https://docs.gitlab.com/user/project/repository/web_editor/#upload-a-file) files
+* [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
+
+```
+cd existing_repo
+git remote add origin http://044b9b313ae1/java/cms.git
+git branch -M main
+git push -uf origin main
+```
+
+## Integrate with your tools
+
+* [Set up project integrations](http://044b9b313ae1/java/cms/-/settings/integrations)
+
+## Collaborate with your team
+
+* [Invite team members and collaborators](https://docs.gitlab.com/user/project/members/)
+* [Create a new merge request](https://docs.gitlab.com/user/project/merge_requests/creating_merge_requests/)
+* [Automatically close issues from merge requests](https://docs.gitlab.com/user/project/issues/managing_issues/#closing-issues-automatically)
+* [Enable merge request approvals](https://docs.gitlab.com/user/project/merge_requests/approvals/)
+* [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
+
+## Test and Deploy
+
+Use the built-in continuous integration in GitLab.
+
+* [Get started with GitLab CI/CD](https://docs.gitlab.com/ci/quick_start/)
+* [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/user/application_security/sast/)
+* [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/topics/autodevops/requirements/)
+* [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/user/clusters/agent/)
+* [Set up protected environments](https://docs.gitlab.com/ci/environments/protected_environments/)
+
+***
+
+# Editing this README
+
+When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
+
+## Suggestions for a good README
+
+Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
+
+## Name
+Choose a self-explaining name for your project.
+
+## Description
+Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
+
+## Badges
+On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
+
+## Visuals
+Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
+
+## Installation
+Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
+
+## Usage
+Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
+
+## Support
+Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
+
+## Roadmap
+If you have ideas for releases in the future, it is a good idea to list them in the README.
+
+## Contributing
+State if you are open to contributions and what your requirements are for accepting them.
+
+For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
+
+You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
+
+## Authors and acknowledgment
+Show your appreciation to those who have contributed to the project.
+
+## License
+For open source projects, say how it is licensed.
+
+## Project status
+If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.

+ 76 - 0
cms/cms-api/pom.xml

@@ -0,0 +1,76 @@
+<?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.cms</groupId>
+        <artifactId>cms</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>cms-api</artifactId>
+    <packaging>jar</packaging>
+
+    <description>cms API 接口模块</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.cms</groupId>
+            <artifactId>cms-entity</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.exchange</groupId>
+            <artifactId>common-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.exchange</groupId>
+            <artifactId>common-log</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.exchange</groupId>
+            <artifactId>common-mybatisPlus</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.exchange</groupId>
+            <artifactId>common-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.exchange</groupId>
+            <artifactId>common-utils</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>${project.name}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.7.18</version>
+                <configuration>
+                    <mainClass>com.cms.CmsApplication</mainClass>
+                    <excludes>
+                        <exclude>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                        </exclude>
+                    </excludes>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>repackage</id>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 14 - 0
cms/cms-api/src/main/java/com/cms/CmsApplication.java

@@ -0,0 +1,14 @@
+package com.cms;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+@MapperScan("com.cms.mapper")
+public class CmsApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(CmsApplication.class, args);
+    }
+}

+ 35 - 0
cms/cms-api/src/main/java/com/cms/config/WebMvcConfig.java

@@ -0,0 +1,35 @@
+package com.cms.config;
+
+import com.cms.interceptor.AuthenticationInterceptor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import javax.annotation.Resource;
+
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+    @Resource
+    private AuthenticationInterceptor authenticationInterceptor;
+
+    private static final String[] EXCLUDE_PATHS = {
+            "/v2/api-docs",
+            "/swagger-resources",
+            "/swagger-resources/**",
+            "/configuration/ui",
+            "/configuration/security",
+            "/swagger-ui.html",
+            "/webjars/**",
+            "/error",
+            // 帮助中心公开接口,无需登录
+            "/help/**"
+    };
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(authenticationInterceptor)
+                .excludePathPatterns(EXCLUDE_PATHS)
+                .addPathPatterns("/**");
+    }
+}

+ 81 - 0
cms/cms-api/src/main/java/com/cms/controller/HelpController.java

@@ -0,0 +1,81 @@
+package com.cms.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.cms.entity.SysHelp;
+import com.cms.entity.SysHelpCategory;
+import com.cms.entity.resp.MessageResult;
+import com.cms.service.SysHelpCategoryService;
+import com.cms.service.SysHelpService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 帮助中心 - 公开查询接口(无需登录)
+ */
+@RestController
+@RequestMapping("/help")
+@Slf4j
+public class HelpController {
+
+    @Autowired
+    private SysHelpService sysHelpService;
+
+    @Autowired
+    private SysHelpCategoryService sysHelpCategoryService;
+
+    /**
+     * 查询所有启用的分类
+     * GET /help/categories?scene=HELP
+     */
+    @GetMapping("/categories")
+    public MessageResult categories(@RequestParam(required = false) String scene) {
+        List<SysHelpCategory> list = (scene != null && !scene.isEmpty())
+                ? sysHelpCategoryService.listEnabledByScene(scene)
+                : sysHelpCategoryService.listEnabled();
+        return MessageResult.success("SUCCESS", list);
+    }
+
+    /**
+     * 按分类分页查询文章
+     * GET /help/page?categoryCode=FAQ&lang=zh_CN&pageNo=1&pageSize=15
+     */
+    @GetMapping("/page")
+    public MessageResult page(
+            @RequestParam(required = false) String categoryCode,
+            @RequestParam(required = false) String lang,
+            @RequestParam(defaultValue = "1") int pageNo,
+            @RequestParam(defaultValue = "15") int pageSize
+    ) {
+        IPage<SysHelp> result = sysHelpService.pageByCategory(pageNo, pageSize, categoryCode, lang);
+        return MessageResult.success("SUCCESS", result);
+    }
+
+    /**
+     * 按分类全量查询(适合协议、免责等少量内容场景)
+     * GET /help/list?categoryCode=PRIVACY&lang=en_US
+     */
+    @GetMapping("/list")
+    public MessageResult list(
+            @RequestParam(required = false) String categoryCode,
+            @RequestParam(required = false) String lang
+    ) {
+        List<SysHelp> list = sysHelpService.listByCategory(categoryCode, lang);
+        return MessageResult.success("SUCCESS", list);
+    }
+
+    /**
+     * 文章详情
+     * GET /help/detail?id=123
+     */
+    @GetMapping("/detail")
+    public MessageResult detail(@RequestParam Long id) {
+        SysHelp help = sysHelpService.getDetail(id);
+        if (help == null || help.getStatus() != 0) {
+            return MessageResult.error("文章不存在或已下线");
+        }
+        return MessageResult.success("SUCCESS", help);
+    }
+}

+ 33 - 0
cms/cms-api/src/main/java/com/cms/interceptor/AuthenticationInterceptor.java

@@ -0,0 +1,33 @@
+package com.cms.interceptor;
+
+import com.cms.entity.transform.AuthMember;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Slf4j
+@Component
+public class AuthenticationInterceptor implements HandlerInterceptor {
+
+    public static final String SESSION_MEMBER = "MEMBER";
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        if (!(handler instanceof HandlerMethod)) {
+            return true;
+        }
+        log.info("sessionId={}", request.getSession().getId());
+        AuthMember authMember = (AuthMember) request.getSession().getAttribute(SESSION_MEMBER);
+        if (authMember == null) {
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+            response.setContentType("application/json;charset=UTF-8");
+            response.getWriter().write("{\"code\":4001,\"message\":\"未登录,请先登录\"}");
+            return false;
+        }
+        return true;
+    }
+}

+ 9 - 0
cms/cms-api/src/main/java/com/cms/mapper/SysHelpCategoryMapper.java

@@ -0,0 +1,9 @@
+package com.cms.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cms.entity.SysHelpCategory;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SysHelpCategoryMapper extends BaseMapper<SysHelpCategory> {
+}

+ 9 - 0
cms/cms-api/src/main/java/com/cms/mapper/SysHelpMapper.java

@@ -0,0 +1,9 @@
+package com.cms.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cms.entity.SysHelp;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SysHelpMapper extends BaseMapper<SysHelp> {
+}

+ 41 - 0
cms/cms-api/src/main/java/com/cms/service/SysHelpCategoryService.java

@@ -0,0 +1,41 @@
+package com.cms.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.cms.entity.SysHelpCategory;
+import com.cms.mapper.SysHelpCategoryMapper;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class SysHelpCategoryService extends ServiceImpl<SysHelpCategoryMapper, SysHelpCategory> {
+
+    /**
+     * 查询所有启用的分类,按 sort 倒序
+     */
+    public List<SysHelpCategory> listEnabled() {
+        return list(new LambdaQueryWrapper<SysHelpCategory>()
+                .eq(SysHelpCategory::getStatus, 1)
+                .orderByDesc(SysHelpCategory::getSort));
+    }
+
+    /**
+     * 按场景查询启用的分类
+     */
+    public List<SysHelpCategory> listEnabledByScene(String scene) {
+        return list(new LambdaQueryWrapper<SysHelpCategory>()
+                .eq(SysHelpCategory::getStatus, 1)
+                .eq(SysHelpCategory::getScene, scene)
+                .orderByDesc(SysHelpCategory::getSort));
+    }
+
+    /**
+     * 按 code 查询
+     */
+    public SysHelpCategory getByCode(String code) {
+        return getOne(new LambdaQueryWrapper<SysHelpCategory>()
+                .eq(SysHelpCategory::getCode, code)
+                .last("limit 1"));
+    }
+}

+ 58 - 0
cms/cms-api/src/main/java/com/cms/service/SysHelpService.java

@@ -0,0 +1,58 @@
+package com.cms.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.cms.entity.SysHelp;
+import com.cms.mapper.SysHelpMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+
+@Service
+public class SysHelpService extends ServiceImpl<SysHelpMapper, SysHelp> {
+
+    /**
+     * 按分类 code + 语言分页查询(已发布)
+     * status=0 表示显示
+     */
+    public IPage<SysHelp> pageByCategory(int pageNo, int pageSize, String categoryCode, String lang) {
+        LambdaQueryWrapper<SysHelp> wrapper = new LambdaQueryWrapper<SysHelp>()
+                .eq(SysHelp::getStatus, 0)
+                .orderByDesc(SysHelp::getIsTop)
+                .orderByDesc(SysHelp::getSort);
+        if (StringUtils.hasText(categoryCode)) {
+            wrapper.eq(SysHelp::getCategoryCode, categoryCode);
+        }
+        if (StringUtils.hasText(lang)) {
+            wrapper.eq(SysHelp::getLang, lang);
+        }
+        return page(new Page<>(pageNo, pageSize), wrapper);
+    }
+
+    /**
+     * 按分类 code + 语言查询全部(用于协议、免责等全量加载场景)
+     */
+    public List<SysHelp> listByCategory(String categoryCode, String lang) {
+        LambdaQueryWrapper<SysHelp> wrapper = new LambdaQueryWrapper<SysHelp>()
+                .eq(SysHelp::getStatus, 0)
+                .orderByDesc(SysHelp::getIsTop)
+                .orderByDesc(SysHelp::getSort);
+        if (StringUtils.hasText(categoryCode)) {
+            wrapper.eq(SysHelp::getCategoryCode, categoryCode);
+        }
+        if (StringUtils.hasText(lang)) {
+            wrapper.eq(SysHelp::getLang, lang);
+        }
+        return list(wrapper);
+    }
+
+    /**
+     * 文章详情
+     */
+    public SysHelp getDetail(Long id) {
+        return getById(id);
+    }
+}

+ 28 - 0
cms/cms-api/src/main/resources/application.yml

@@ -0,0 +1,28 @@
+server:
+  port: 6029
+spring:
+  datasource:
+    dynamic:
+      primary: cms             # 默认数据源
+      strict: false
+      datasource:
+        cms:
+          driver-class-name: com.mysql.cj.jdbc.Driver
+          url: jdbc:mysql://13.213.125.43:3306/kucoin?characterEncoding=utf-8&serverTimezone=GMT%2B8&useSSL=false
+          username: kucoin
+          password: KCrbYZ2XcjBtxzdG
+  redis:
+    host: 13.213.125.43
+    port: 6379
+    password: redis123!
+    database: 0
+    timeout: 5000ms
+    lettuce:
+      pool:
+        max-active: 8
+        max-idle: 8
+        min-idle: 1
+        max-wait: 2000
+
+mybatis-plus:
+  mapper-locations: classpath*:mapper/**/*.xml

+ 88 - 0
cms/cms-api/src/main/resources/log4j2-spring.xml

@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<Configuration status="WARN" monitorInterval="30">
+    <Properties>
+        <Property name="app.name">${sys:APP_NAME:-${env:APP_NAME:-application}}</Property>
+        <Property name="log.path">${sys:LOG_PATH:-${env:LOG_PATH:-${sys:user.home}/logs}}</Property>
+        <Property name="log.roll.pattern">${sys:LOG_ROLL_PATTERN:-${env:LOG_ROLL_PATTERN:-yyyy-MM-dd}}</Property>
+        <Property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</Property>
+        <Property name="BYTEPLUS_TLS_ENDPOINT">${env:BYTEPLUS_TLS_ENDPOINT:-${env:VOLCENGINE_ENDPOINT:-}}</Property>
+        <Property name="BYTEPLUS_TLS_ACCESS_KEY_ID">${env:BYTEPLUS_TLS_ACCESS_KEY_ID:-${env:VOLCENGINE_ACCESS_KEY_ID:-}}</Property>
+        <Property name="BYTEPLUS_TLS_ACCESS_KEY_SECRET">${env:BYTEPLUS_TLS_ACCESS_KEY_SECRET:-${env:VOLCENGINE_ACCESS_KEY_SECRET:-}}</Property>
+        <Property name="BYTEPLUS_TLS_REGION">${env:BYTEPLUS_TLS_REGION:-${env:VOLCENGINE_REGION:-ap-southeast-1}}</Property>
+        <Property name="TLS_PROJECT_ID">${sys:byteplus.tls.projectId:-}</Property>
+        <Property name="TLS_TOPIC_ID">${sys:byteplus.tls.topicId:-}</Property>
+        <Property name="BYTEPLUS_TLS_BATCH_SIZE">${env:BYTEPLUS_TLS_BATCH_SIZE:-100}</Property>
+        <Property name="BYTEPLUS_TLS_FLUSH_INTERVAL_SECONDS">${env:BYTEPLUS_TLS_FLUSH_INTERVAL_SECONDS:-5}</Property>
+        <Property name="mybatis.sql.log.level">${sys:MYBATIS_SQL_LOG_LEVEL:-${env:MYBATIS_SQL_LOG_LEVEL:-INFO}}</Property>
+    </Properties>
+
+    <Appenders>
+        <Console name="Console" target="SYSTEM_OUT">
+            <PatternLayout pattern="${LOG_PATTERN}"/>
+        </Console>
+
+        <!-- 按天生成日志,直接写入日期子目录(启动时即创建当天目录,与 logback 行为一致) -->
+        <RollingFile name="RollingLogFile"
+                     filePattern="${log.path}/${app.name}/%d{${log.roll.pattern}}/%d{${log.roll.pattern}}-%i.log">
+            <PatternLayout pattern="${LOG_PATTERN}"/>
+            <Policies>
+                <TimeBasedTriggeringPolicy/>
+                <SizeBasedTriggeringPolicy size="200MB"/>
+            </Policies>
+            <DirectWriteRolloverStrategy maxFiles="10"/>
+        </RollingFile>
+
+        <Async name="AsyncRollingLogFile" bufferSize="262144">
+            <AppenderRef ref="RollingLogFile"/>
+        </Async>
+
+        <BytePlusTLS name="BytePlusTLS"
+                     endpoint="${BYTEPLUS_TLS_ENDPOINT}"
+                     accessKeyId="${BYTEPLUS_TLS_ACCESS_KEY_ID}"
+                     accessKeySecret="${BYTEPLUS_TLS_ACCESS_KEY_SECRET}"
+                     region="${BYTEPLUS_TLS_REGION}"
+                     projectId="${TLS_PROJECT_ID}"
+                     topicId="${TLS_TOPIC_ID}"
+                     batchSize="${BYTEPLUS_TLS_BATCH_SIZE}"
+                     flushIntervalSeconds="${BYTEPLUS_TLS_FLUSH_INTERVAL_SECONDS}">
+            <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
+        </BytePlusTLS>
+    </Appenders>
+
+    <Loggers>
+        <Logger name="org.springframework" level="INFO" additivity="false">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="AsyncRollingLogFile"/>
+        </Logger>
+        <Logger name="com.alibaba.nacos" level="INFO" additivity="false">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="AsyncRollingLogFile"/>
+        </Logger>
+        <Logger name="com.volcengine" level="INFO" additivity="false">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="AsyncRollingLogFile"/>
+        </Logger>
+        <Logger name="com.exchange.log.appender" level="INFO" additivity="false">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="AsyncRollingLogFile"/>
+        </Logger>
+        <Logger name="org.apache.ibatis" level="${mybatis.sql.log.level}" additivity="false">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="AsyncRollingLogFile"/>
+        </Logger>
+        <Logger name="org.apache.ibatis.logging.jdbc" level="${mybatis.sql.log.level}" additivity="false">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="AsyncRollingLogFile"/>
+        </Logger>
+        <Logger name="com.exchange" level="${mybatis.sql.log.level}" additivity="false">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="AsyncRollingLogFile"/>
+            <AppenderRef ref="BytePlusTLS"/>
+        </Logger>
+        <Root level="INFO">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="AsyncRollingLogFile"/>
+        </Root>
+    </Loggers>
+</Configuration>

+ 28 - 0
cms/cms-entity/pom.xml

@@ -0,0 +1,28 @@
+<?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.cms</groupId>
+        <artifactId>cms</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>cms-entity</artifactId>
+    <packaging>jar</packaging>
+
+    <description>cms 实体模块</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.exchange</groupId>
+            <artifactId>common-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.exchange</groupId>
+            <artifactId>common-mybatisPlus</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 49 - 0
cms/cms-entity/src/main/java/com/cms/entity/SysHelp.java

@@ -0,0 +1,49 @@
+package com.cms.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 系统帮助文章
+ */
+@Data
+@TableName("sys_help")
+public class SysHelp {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 标题 */
+    private String title;
+
+    /** 分类编码,关联 sys_help_category.code */
+    private String categoryCode;
+
+    /** 图片地址 */
+    private String imgUrl;
+
+    /** 文章内容 */
+    @TableField(value = "content")
+    private String content;
+
+    /** 作者 */
+    private String author;
+
+    /** 排序值 */
+    private Integer sort;
+
+    /** 是否置顶: 0=置顶, 1=不置顶 */
+    private String isTop;
+
+    /** 文章语言,如 zh_CN / en_US / ja_JP */
+    private String lang;
+
+    /** 状态: 0=显示, 1=不显示(与 doge 项目保持一致) */
+    private Integer status;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+}

+ 47 - 0
cms/cms-entity/src/main/java/com/cms/entity/SysHelpCategory.java

@@ -0,0 +1,47 @@
+package com.cms.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 帮助文章分类枚举表
+ */
+@Data
+@TableName("sys_help_category")
+public class SysHelpCategory {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 分类编码(唯一英文标识) */
+    private String code;
+
+    /** 中文名称 */
+    private String nameZh;
+
+    /** 英文名称 */
+    private String nameEn;
+
+    /** 适用场景: HELP / PROTOCOL / NOTICE */
+    private String scene;
+
+    /** 排序值,越大越靠前 */
+    private Integer sort;
+
+    /** 状态: 1=启用, 0=禁用 */
+    private Integer status;
+
+    /** 备注 */
+    private String remark;
+
+    @TableField(fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime updateTime;
+}

+ 69 - 0
cms/cms-entity/src/main/java/com/cms/entity/resp/MessageResult.java

@@ -0,0 +1,69 @@
+package com.cms.entity.resp;
+
+import com.alibaba.fastjson2.JSONObject;
+
+public class MessageResult<T> {
+    private T data;
+    public MessageResult(int code , String msg){
+        this.code = code;
+        this.message = msg;
+    }
+    public MessageResult(int code , String msg, T object){
+        this.code = code;
+        this.message = msg;
+        this.data = object;
+    }
+    public MessageResult() {
+    }
+
+    public MessageResult(JSONObject json) {
+        this.code = json.getInteger("code");
+        this.message = json.getString("message");
+        if(json.containsKey("data"))
+            this.setData((T)json.get("data"));
+    }
+
+    public static MessageResult success(){
+        return new MessageResult(0,"SUCCESS");
+    }
+    public static MessageResult success(String msg){
+        return new MessageResult(0,msg);
+    }
+    public static MessageResult success(String msg,Object data){
+        return new MessageResult(0,msg,data);
+    }
+
+    public static MessageResult error(int code,String msg){
+        return new MessageResult(code,msg);
+    }
+    public static MessageResult error(String msg){
+        return new MessageResult(500,msg);
+    }
+
+    private int code;
+    private String message;
+    private String totalPage;
+    private String totalElement;
+
+    public String getTotalPage() { return totalPage; }
+    public void setTotalPage(String totalPage) { this.totalPage = totalPage; }
+    public String getTotalElement() { return totalElement; }
+    public void setTotalElement(String totalElement) { this.totalElement = totalElement; }
+    public int getCode() { return code; }
+    public void setCode(int code) { this.code = code; }
+    public String getMessage() { return message; }
+    public void setMessage(String message) { this.message = message; }
+
+    @Override
+    public String toString(){
+        return JSONObject.toJSONString(this);
+    }
+    public Object getData() { return data; }
+    public void setData(T data) { this.data = data; }
+
+    public static MessageResult getSuccessInstance(String message, Object data){
+        MessageResult result = success(message);
+        result.setData(data);
+        return result;
+    }
+}

+ 16 - 0
cms/cms-entity/src/main/java/com/cms/entity/transform/AuthMember.java

@@ -0,0 +1,16 @@
+package com.cms.entity.transform;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class AuthMember implements Serializable {
+    private static final long serialVersionUID = -4199550203850153635L;
+    private long id;
+    private String name;
+    private String realName;
+    private String mobilePhone;
+    private String email;
+    private String authToken;
+}

+ 116 - 0
cms/pom.xml

@@ -0,0 +1,116 @@
+<?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>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.7.18</version>
+        <relativePath/>
+    </parent>
+
+    <groupId>com.cms</groupId>
+    <artifactId>cms</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>pom</packaging>
+
+    <description>cms 内容管理系统</description>
+
+    <modules>
+        <module>cms-entity</module>
+        <module>cms-api</module>
+    </modules>
+
+    <properties>
+        <java.version>17</java.version>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <spring-cloud.version>2021.0.8</spring-cloud.version>
+        <spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
+        <common.version>1.0.0-SNAPSHOT</common.version>
+        <lombok.version>1.18.30</lombok.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <!-- Spring Cloud -->
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-dependencies</artifactId>
+                <version>${spring-cloud.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <!-- Spring Cloud Alibaba -->
+            <dependency>
+                <groupId>com.alibaba.cloud</groupId>
+                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
+                <version>${spring-cloud-alibaba.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <!-- Common 包版本管理 -->
+            <dependency>
+                <groupId>com.exchange</groupId>
+                <artifactId>common-core</artifactId>
+                <version>${common.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.exchange</groupId>
+                <artifactId>common-log</artifactId>
+                <version>${common.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.exchange</groupId>
+                <artifactId>common-redis</artifactId>
+                <version>${common.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.exchange</groupId>
+                <artifactId>common-mybatisPlus</artifactId>
+                <version>${common.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.exchange</groupId>
+                <artifactId>common-utils</artifactId>
+                <version>${common.version}</version>
+            </dependency>
+            <!-- 多数据源 -->
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
+                <version>3.6.1</version>
+            </dependency>
+            <!-- 子模块版本管理 -->
+            <dependency>
+                <groupId>com.cms</groupId>
+                <artifactId>cms-entity</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+    <distributionManagement>
+        <repository>
+            <id>nexus-releases</id>
+            <name>Nexus Release Repository</name>
+            <url>http://43.99.99.42:18081/repository/maven-releases/</url>
+        </repository>
+        <snapshotRepository>
+            <id>maven-snapshots</id>
+            <name>Nexus Snapshot Repository</name>
+            <url>http://43.99.99.42:18081/repository/maven-snapshots/</url>
+        </snapshotRepository>
+    </distributionManagement>
+    <dependencies>
+        <!-- Lombok 所有模块共享 -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>${lombok.version}</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+</project>

+ 3 - 0
contract/.gitignore

@@ -0,0 +1,3 @@
+.idea/
+**/target/
+**/*.class

+ 93 - 0
contract/README.md

@@ -0,0 +1,93 @@
+# contrace
+
+
+
+## Getting started
+
+To make it easy for you to get started with GitLab, here's a list of recommended next steps.
+
+Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
+
+## Add your files
+
+* [Create](https://docs.gitlab.com/user/project/repository/web_editor/#create-a-file) or [upload](https://docs.gitlab.com/user/project/repository/web_editor/#upload-a-file) files
+* [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
+
+```
+cd existing_repo
+git remote add origin http://044b9b313ae1/java/contrace.git
+git branch -M main
+git push -uf origin main
+```
+
+## Integrate with your tools
+
+* [Set up project integrations](http://044b9b313ae1/java/contrace/-/settings/integrations)
+
+## Collaborate with your team
+
+* [Invite team members and collaborators](https://docs.gitlab.com/user/project/members/)
+* [Create a new merge request](https://docs.gitlab.com/user/project/merge_requests/creating_merge_requests/)
+* [Automatically close issues from merge requests](https://docs.gitlab.com/user/project/issues/managing_issues/#closing-issues-automatically)
+* [Enable merge request approvals](https://docs.gitlab.com/user/project/merge_requests/approvals/)
+* [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
+
+## Test and Deploy
+
+Use the built-in continuous integration in GitLab.
+
+* [Get started with GitLab CI/CD](https://docs.gitlab.com/ci/quick_start/)
+* [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/user/application_security/sast/)
+* [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/topics/autodevops/requirements/)
+* [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/user/clusters/agent/)
+* [Set up protected environments](https://docs.gitlab.com/ci/environments/protected_environments/)
+
+***
+
+# Editing this README
+
+When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
+
+## Suggestions for a good README
+
+Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
+
+## Name
+Choose a self-explaining name for your project.
+
+## Description
+Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
+
+## Badges
+On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
+
+## Visuals
+Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
+
+## Installation
+Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
+
+## Usage
+Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
+
+## Support
+Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
+
+## Roadmap
+If you have ideas for releases in the future, it is a good idea to list them in the README.
+
+## Contributing
+State if you are open to contributions and what your requirements are for accepting them.
+
+For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
+
+You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
+
+## Authors and acknowledgment
+Show your appreciation to those who have contributed to the project.
+
+## License
+For open source projects, say how it is licensed.
+
+## Project status
+If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.

+ 80 - 0
contract/contract-api/pom.xml

@@ -0,0 +1,80 @@
+<?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.contract</groupId>
+        <artifactId>contract</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>contract-api</artifactId>
+    <packaging>jar</packaging>
+
+    <description>contract API 接口模块</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.contract</groupId>
+            <artifactId>contract-entity</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.exchange</groupId>
+            <artifactId>common-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.exchange</groupId>
+            <artifactId>common-log</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.exchange</groupId>
+            <artifactId>common-mybatisPlus</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.exchange</groupId>
+            <artifactId>common-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.exchange</groupId>
+            <artifactId>common-utils</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>${project.name}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.7.18</version>
+                <configuration>
+                    <mainClass>com.contract.ContractApplication</mainClass>
+                    <excludes>
+                        <exclude>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                        </exclude>
+                    </excludes>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>repackage</id>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 12 - 0
contract/contract-api/src/main/java/com/contract/ContractApplication.java

@@ -0,0 +1,12 @@
+package com.contract;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication(scanBasePackages = {"com.contract", "com.exchange"})
+public class ContractApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(ContractApplication.class, args);
+    }
+}

+ 54 - 0
contract/contract-api/src/main/java/com/contract/config/WebMvcConfig.java

@@ -0,0 +1,54 @@
+package com.contract.config;
+
+import com.contract.interceptor.AuthenticationInterceptor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import javax.annotation.Resource;
+
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+    @Resource
+    private AuthenticationInterceptor authenticationInterceptor;
+
+    private static final String[] EXCLUDE_PATHS = {
+            "/v2/api-docs",
+            "/swagger-resources",
+            "/swagger-resources/**",
+            "/configuration/ui",
+            "/configuration/security",
+            "/swagger-ui.html",
+            "/webjars/**",
+            "/error",
+            "/watchlist/test",
+            "/node/list",
+            "/app-banner/active-list",
+            "/app-banner/page-query",
+            "/app-banner/detail",
+            "/app-header/active-list",
+            "/app-header/page-query",
+            "/app-header/detail",
+            "/currency/rate",
+            "/coin-ext/detail"
+    };
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+                .allowedOriginPatterns("*")
+                .allowedMethods("*")
+                .allowedHeaders("*")
+                .allowCredentials(true)
+                .maxAge(3600);
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(authenticationInterceptor)
+                .excludePathPatterns(EXCLUDE_PATHS)
+                .addPathPatterns("/**");
+    }
+}

+ 57 - 0
contract/contract-api/src/main/java/com/contract/controller/AppBannerController.java

@@ -0,0 +1,57 @@
+package com.contract.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.contract.entity.AppBanner;
+import com.contract.entity.resp.MessageResult;
+import com.contract.enums.MessageCode;
+import com.contract.service.AppBannerService;
+import com.contract.util.RequestUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+
+@RestController
+@RequestMapping("/app-banner")
+public class AppBannerController {
+
+    @Autowired
+    private AppBannerService appBannerService;
+
+    /**
+     * 分页查询
+     */
+    @PostMapping("/page-query")
+    public MessageResult pageQuery(
+            @RequestParam(defaultValue = "1") int pageNum,
+            @RequestParam(defaultValue = "10") int pageSize,
+            @RequestParam(required = false) Integer status,
+            @RequestParam(required = false) String title,
+            HttpServletRequest request) {
+        String lang = RequestUtils.getLangFromHeader(request);
+        Page<AppBanner> page = appBannerService.pageQuery(pageNum, pageSize, status, title);
+        return MessageResult.successLang(lang, page);
+    }
+
+    /**
+     * 查询详情
+     */
+    @GetMapping("/detail")
+    public MessageResult detail(@RequestParam Long id, HttpServletRequest request) {
+        String lang = RequestUtils.getLangFromHeader(request);
+        AppBanner banner = appBannerService.getById(id);
+        if (banner == null) {
+            return MessageResult.failLang(lang, MessageCode.BANNER_NOT_FOUND);
+        }
+        return MessageResult.successLang(lang, banner);
+    }
+
+    /**
+     * 查询App端生效的Banner列表
+     */
+    @GetMapping("/active-list")
+    public MessageResult activeList(HttpServletRequest request) {
+        String lang = RequestUtils.getLangFromHeader(request);
+        return MessageResult.successLang(lang, appBannerService.findActiveBanners());
+    }
+}

+ 57 - 0
contract/contract-api/src/main/java/com/contract/controller/AppHeaderController.java

@@ -0,0 +1,57 @@
+package com.contract.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.contract.entity.AppHeader;
+import com.contract.entity.resp.MessageResult;
+import com.contract.enums.MessageCode;
+import com.contract.service.AppHeaderService;
+import com.contract.util.RequestUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+
+@RestController
+@RequestMapping("/app-header")
+public class AppHeaderController {
+
+    @Autowired
+    private AppHeaderService appHeaderService;
+
+    /**
+     * 分页查询
+     */
+    @PostMapping("/page-query")
+    public MessageResult pageQuery(
+            @RequestParam(defaultValue = "1") int pageNum,
+            @RequestParam(defaultValue = "10") int pageSize,
+            @RequestParam(required = false) Integer status,
+            @RequestParam(required = false) String title,
+            HttpServletRequest request) {
+        String lang = RequestUtils.getLangFromHeader(request);
+        Page<AppHeader> page = appHeaderService.pageQuery(pageNum, pageSize, status, title);
+        return MessageResult.successLang(lang, page);
+    }
+
+    /**
+     * 查询详情
+     */
+    @GetMapping("/detail")
+    public MessageResult detail(@RequestParam Long id, HttpServletRequest request) {
+        String lang = RequestUtils.getLangFromHeader(request);
+        AppHeader header = appHeaderService.getById(id);
+        if (header == null) {
+            return MessageResult.failLang(lang, MessageCode.HEADER_NOT_FOUND);
+        }
+        return MessageResult.successLang(lang, header);
+    }
+
+    /**
+     * 查询App端生效的Header列表
+     */
+    @GetMapping("/active-list")
+    public MessageResult activeList(HttpServletRequest request) {
+        String lang = RequestUtils.getLangFromHeader(request);
+        return MessageResult.successLang(lang, appHeaderService.findActiveHeaders());
+    }
+}

+ 28 - 0
contract/contract-api/src/main/java/com/contract/controller/CoinExtController.java

@@ -0,0 +1,28 @@
+package com.contract.controller;
+
+import com.contract.entity.CoinExt;
+import com.contract.entity.resp.MessageResult;
+import com.contract.service.CoinExtService;
+import com.contract.util.RequestUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+
+@RestController
+@RequestMapping("/coin-ext")
+public class CoinExtController {
+
+    @Autowired
+    private CoinExtService coinExtService;
+
+    @GetMapping("/detail")
+    public MessageResult detail(@RequestParam String symbol, HttpServletRequest request) {
+        String lang = RequestUtils.getLangFromHeader(request);
+        CoinExt coinExt = coinExtService.getBySymbol(symbol);
+        return MessageResult.successLang(lang, coinExt);
+    }
+}

+ 29 - 0
contract/contract-api/src/main/java/com/contract/controller/CurrencyCoinController.java

@@ -0,0 +1,29 @@
+package com.contract.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.contract.entity.CurrencyCoin;
+import com.contract.entity.resp.MessageResult;
+import com.contract.service.CurrencyCoinService;
+import com.contract.util.RequestUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+
+@RestController
+@RequestMapping("/currency")
+public class CurrencyCoinController {
+
+    @Autowired
+    private CurrencyCoinService currencyCoinService;
+
+    @GetMapping("/rate")
+    public MessageResult rate(HttpServletRequest request) {
+        String lang = RequestUtils.getLangFromHeader(request);
+        List<CurrencyCoin> coin = currencyCoinService.list(new LambdaQueryWrapper<>());
+        return MessageResult.successLang(lang, coin);
+    }
+}

+ 28 - 0
contract/contract-api/src/main/java/com/contract/controller/NodeInfoController.java

@@ -0,0 +1,28 @@
+package com.contract.controller;
+
+import com.contract.entity.NodeInfo;
+import com.contract.entity.resp.MessageResult;
+import com.contract.service.NodeInfoService;
+import com.contract.util.RequestUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+
+@RestController
+@RequestMapping("/node")
+public class NodeInfoController {
+
+    @Autowired
+    private NodeInfoService nodeInfoService;
+
+    @GetMapping("/list")
+    public MessageResult list(HttpServletRequest request) {
+        String lang = RequestUtils.getLangFromHeader(request);
+        List<NodeInfo> list = nodeInfoService.listEnabled();
+        return MessageResult.successLang(lang, list);
+    }
+}

+ 52 - 0
contract/contract-api/src/main/java/com/contract/controller/UserWatchlistController.java

@@ -0,0 +1,52 @@
+package com.contract.controller;
+
+import com.contract.entity.UserWatchlist;
+import com.contract.entity.resp.MessageResult;
+import com.contract.entity.transform.AuthMember;
+import com.contract.interceptor.AuthenticationInterceptor;
+import com.contract.service.UserWatchlistService;
+import com.contract.util.RequestUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import java.util.List;
+
+@RestController
+@RequestMapping("/watchlist")
+public class UserWatchlistController {
+
+    @Autowired
+    private UserWatchlistService userWatchlistService;
+
+    @GetMapping("/test")
+    public MessageResult test(HttpServletRequest request) {
+        String lang = RequestUtils.getLangFromHeader(request);
+        return MessageResult.successLang(lang);
+    }
+
+    @GetMapping("/list")
+    public MessageResult list(HttpSession session, HttpServletRequest request) {
+        String lang = RequestUtils.getLangFromHeader(request);
+        AuthMember authMember = (AuthMember) session.getAttribute(AuthenticationInterceptor.SESSION_MEMBER);
+        List<UserWatchlist> list = userWatchlistService.listByMember(authMember.getId());
+        return MessageResult.successLang(lang, list);
+    }
+
+    @PostMapping("/add")
+    public MessageResult add(@RequestBody UserWatchlist req, HttpSession session, HttpServletRequest request) {
+        String lang = RequestUtils.getLangFromHeader(request);
+        AuthMember authMember = (AuthMember) session.getAttribute(AuthenticationInterceptor.SESSION_MEMBER);
+        userWatchlistService.addWatchlist(authMember.getId(), req.getSymbol());
+        return MessageResult.successLang(lang);
+    }
+
+    @PostMapping("/cancel")
+    public MessageResult cancel(@RequestBody UserWatchlist req, HttpSession session, HttpServletRequest request) {
+        String lang = RequestUtils.getLangFromHeader(request);
+        AuthMember authMember = (AuthMember) session.getAttribute(AuthenticationInterceptor.SESSION_MEMBER);
+        userWatchlistService.cancelWatchlist(authMember.getId(), req.getSymbol());
+        return MessageResult.successLang(lang);
+    }
+}

+ 33 - 0
contract/contract-api/src/main/java/com/contract/interceptor/AuthenticationInterceptor.java

@@ -0,0 +1,33 @@
+package com.contract.interceptor;
+
+import com.contract.entity.transform.AuthMember;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Slf4j
+@Component
+public class AuthenticationInterceptor implements HandlerInterceptor {
+
+    public static final String SESSION_MEMBER = "MEMBER";
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        if (!(handler instanceof HandlerMethod)) {
+            return true;
+        }
+        log.info("sessionId={}", request.getSession().getId());
+        AuthMember authMember = (AuthMember) request.getSession().getAttribute(SESSION_MEMBER);
+        if (authMember == null) {
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+            response.setContentType("application/json;charset=UTF-8");
+            response.getWriter().write("{\"code\":4001,\"message\":\"未登录,请先登录\"}");
+            return false;
+        }
+        return true;
+    }
+}

+ 18 - 0
contract/contract-api/src/main/java/com/contract/mapper/AppBannerMapper.java

@@ -0,0 +1,18 @@
+package com.contract.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.contract.entity.AppBanner;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface AppBannerMapper extends BaseMapper<AppBanner> {
+
+    @Select("SELECT * FROM app_banner WHERE status = 1 " +
+            "AND (start_time IS NULL OR start_time <= NOW()) " +
+            "AND (end_time IS NULL OR end_time >= NOW()) " +
+            "ORDER BY sort DESC, id DESC")
+    List<AppBanner> findActiveBanners();
+}

+ 18 - 0
contract/contract-api/src/main/java/com/contract/mapper/AppHeaderMapper.java

@@ -0,0 +1,18 @@
+package com.contract.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.contract.entity.AppHeader;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface AppHeaderMapper extends BaseMapper<AppHeader> {
+
+    @Select("SELECT * FROM app_header WHERE status = 1 " +
+            "AND (start_time IS NULL OR start_time <= NOW()) " +
+            "AND (end_time IS NULL OR end_time >= NOW()) " +
+            "ORDER BY sort DESC, id DESC")
+    List<AppHeader> findActiveHeaders();
+}

+ 9 - 0
contract/contract-api/src/main/java/com/contract/mapper/CoinExtMapper.java

@@ -0,0 +1,9 @@
+package com.contract.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.contract.entity.CoinExt;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface CoinExtMapper extends BaseMapper<CoinExt> {
+}

+ 9 - 0
contract/contract-api/src/main/java/com/contract/mapper/CurrencyCoinMapper.java

@@ -0,0 +1,9 @@
+package com.contract.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.contract.entity.CurrencyCoin;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface CurrencyCoinMapper extends BaseMapper<CurrencyCoin> {
+}

+ 9 - 0
contract/contract-api/src/main/java/com/contract/mapper/NodeInfoMapper.java

@@ -0,0 +1,9 @@
+package com.contract.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.contract.entity.NodeInfo;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface NodeInfoMapper extends BaseMapper<NodeInfo> {
+}

+ 9 - 0
contract/contract-api/src/main/java/com/contract/mapper/UserWatchlistMapper.java

@@ -0,0 +1,9 @@
+package com.contract.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.contract.entity.UserWatchlist;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface UserWatchlistMapper extends BaseMapper<UserWatchlist> {
+}

+ 14 - 0
contract/contract-api/src/main/java/com/contract/service/AppBannerService.java

@@ -0,0 +1,14 @@
+package com.contract.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.contract.entity.AppBanner;
+
+import java.util.List;
+
+public interface AppBannerService extends IService<AppBanner> {
+
+    Page<AppBanner> pageQuery(int pageNum, int pageSize, Integer status, String title);
+
+    List<AppBanner> findActiveBanners();
+}

+ 14 - 0
contract/contract-api/src/main/java/com/contract/service/AppHeaderService.java

@@ -0,0 +1,14 @@
+package com.contract.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.contract.entity.AppHeader;
+
+import java.util.List;
+
+public interface AppHeaderService extends IService<AppHeader> {
+
+    Page<AppHeader> pageQuery(int pageNum, int pageSize, Integer status, String title);
+
+    List<AppHeader> findActiveHeaders();
+}

+ 9 - 0
contract/contract-api/src/main/java/com/contract/service/CoinExtService.java

@@ -0,0 +1,9 @@
+package com.contract.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.contract.entity.CoinExt;
+
+public interface CoinExtService extends IService<CoinExt> {
+
+    CoinExt getBySymbol(String symbol);
+}

+ 8 - 0
contract/contract-api/src/main/java/com/contract/service/CurrencyCoinService.java

@@ -0,0 +1,8 @@
+package com.contract.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.contract.entity.CurrencyCoin;
+
+public interface CurrencyCoinService extends IService<CurrencyCoin> {
+
+}

+ 11 - 0
contract/contract-api/src/main/java/com/contract/service/NodeInfoService.java

@@ -0,0 +1,11 @@
+package com.contract.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.contract.entity.NodeInfo;
+
+import java.util.List;
+
+public interface NodeInfoService extends IService<NodeInfo> {
+
+    List<NodeInfo> listEnabled();
+}

+ 15 - 0
contract/contract-api/src/main/java/com/contract/service/UserWatchlistService.java

@@ -0,0 +1,15 @@
+package com.contract.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.contract.entity.UserWatchlist;
+
+import java.util.List;
+
+public interface UserWatchlistService extends IService<UserWatchlist> {
+
+    List<UserWatchlist> listByMember(Long memberId);
+
+    void addWatchlist(Long memberId, String symbol);
+
+    void cancelWatchlist(Long memberId, String symbol);
+}

+ 35 - 0
contract/contract-api/src/main/java/com/contract/service/impl/AppBannerServiceImpl.java

@@ -0,0 +1,35 @@
+package com.contract.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.contract.entity.AppBanner;
+import com.contract.mapper.AppBannerMapper;
+import com.contract.service.AppBannerService;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class AppBannerServiceImpl extends ServiceImpl<AppBannerMapper, AppBanner>
+        implements AppBannerService {
+
+    @Override
+    public Page<AppBanner> pageQuery(int pageNum, int pageSize, Integer status, String title) {
+        LambdaQueryWrapper<AppBanner> wrapper = new LambdaQueryWrapper<>();
+        if (status != null) {
+            wrapper.eq(AppBanner::getStatus, status);
+        }
+        if (StringUtils.isNotBlank(title)) {
+            wrapper.like(AppBanner::getTitle, title);
+        }
+        wrapper.orderByDesc(AppBanner::getSort);
+        return page(new Page<>(pageNum, pageSize), wrapper);
+    }
+
+    @Override
+    public List<AppBanner> findActiveBanners() {
+        return baseMapper.findActiveBanners();
+    }
+}

+ 35 - 0
contract/contract-api/src/main/java/com/contract/service/impl/AppHeaderServiceImpl.java

@@ -0,0 +1,35 @@
+package com.contract.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.contract.entity.AppHeader;
+import com.contract.mapper.AppHeaderMapper;
+import com.contract.service.AppHeaderService;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class AppHeaderServiceImpl extends ServiceImpl<AppHeaderMapper, AppHeader>
+        implements AppHeaderService {
+
+    @Override
+    public Page<AppHeader> pageQuery(int pageNum, int pageSize, Integer status, String title) {
+        LambdaQueryWrapper<AppHeader> wrapper = new LambdaQueryWrapper<>();
+        if (status != null) {
+            wrapper.eq(AppHeader::getStatus, status);
+        }
+        if (StringUtils.isNotBlank(title)) {
+            wrapper.like(AppHeader::getTitle, title);
+        }
+        wrapper.orderByDesc(AppHeader::getSort);
+        return page(new Page<>(pageNum, pageSize), wrapper);
+    }
+
+    @Override
+    public List<AppHeader> findActiveHeaders() {
+        return baseMapper.findActiveHeaders();
+    }
+}

+ 19 - 0
contract/contract-api/src/main/java/com/contract/service/impl/CoinExtServiceImpl.java

@@ -0,0 +1,19 @@
+package com.contract.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.contract.entity.CoinExt;
+import com.contract.mapper.CoinExtMapper;
+import com.contract.service.CoinExtService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class CoinExtServiceImpl extends ServiceImpl<CoinExtMapper, CoinExt>
+        implements CoinExtService {
+
+    @Override
+    public CoinExt getBySymbol(String symbol) {
+        return getOne(new LambdaQueryWrapper<CoinExt>()
+                .eq(CoinExt::getSymbol, symbol));
+    }
+}

+ 13 - 0
contract/contract-api/src/main/java/com/contract/service/impl/CurrencyCoinServiceImpl.java

@@ -0,0 +1,13 @@
+package com.contract.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.contract.entity.CurrencyCoin;
+import com.contract.mapper.CurrencyCoinMapper;
+import com.contract.service.CurrencyCoinService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class CurrencyCoinServiceImpl extends ServiceImpl<CurrencyCoinMapper, CurrencyCoin>
+        implements CurrencyCoinService {
+}

+ 21 - 0
contract/contract-api/src/main/java/com/contract/service/impl/NodeInfoServiceImpl.java

@@ -0,0 +1,21 @@
+package com.contract.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.contract.entity.NodeInfo;
+import com.contract.mapper.NodeInfoMapper;
+import com.contract.service.NodeInfoService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class NodeInfoServiceImpl extends ServiceImpl<NodeInfoMapper, NodeInfo>
+        implements NodeInfoService {
+
+    @Override
+    public List<NodeInfo> listEnabled() {
+        return list(new LambdaQueryWrapper<NodeInfo>()
+                .eq(NodeInfo::getStatus, 1));
+    }
+}

+ 59 - 0
contract/contract-api/src/main/java/com/contract/service/impl/UserWatchlistServiceImpl.java

@@ -0,0 +1,59 @@
+package com.contract.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.contract.entity.UserWatchlist;
+import com.contract.mapper.UserWatchlistMapper;
+import com.contract.service.UserWatchlistService;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Service
+public class UserWatchlistServiceImpl extends ServiceImpl<UserWatchlistMapper, UserWatchlist>
+        implements UserWatchlistService {
+
+    @Override
+    public List<UserWatchlist> listByMember(Long memberId) {
+        return list(new LambdaQueryWrapper<UserWatchlist>()
+                .eq(UserWatchlist::getMemberId, memberId)
+                .eq(UserWatchlist::getStatus, 1)
+                .orderByDesc(UserWatchlist::getSort)
+                .orderByDesc(UserWatchlist::getCreatedTime));
+    }
+
+    @Override
+    public void addWatchlist(Long memberId, String symbol) {
+        UserWatchlist existing = getOne(new LambdaQueryWrapper<UserWatchlist>()
+                .eq(UserWatchlist::getMemberId, memberId)
+                .eq(UserWatchlist::getSymbol, symbol));
+        LocalDateTime now = LocalDateTime.now();
+        if (existing == null) {
+            UserWatchlist record = new UserWatchlist();
+            record.setMemberId(memberId);
+            record.setSymbol(symbol);
+            record.setStatus(1);
+            record.setSort(0);
+            record.setCreatedTime(now);
+            record.setUpdatedTime(now);
+            save(record);
+        } else {
+            update(new LambdaUpdateWrapper<UserWatchlist>()
+                    .eq(UserWatchlist::getMemberId, memberId)
+                    .eq(UserWatchlist::getSymbol, symbol)
+                    .set(UserWatchlist::getStatus, 1)
+                    .set(UserWatchlist::getUpdatedTime, now));
+        }
+    }
+
+    @Override
+    public void cancelWatchlist(Long memberId, String symbol) {
+        update(new LambdaUpdateWrapper<UserWatchlist>()
+                .eq(UserWatchlist::getMemberId, memberId)
+                .eq(UserWatchlist::getSymbol, symbol)
+                .set(UserWatchlist::getStatus, 2)
+                .set(UserWatchlist::getUpdatedTime, LocalDateTime.now()));
+    }
+}

+ 25 - 0
contract/contract-api/src/main/java/com/contract/util/RequestUtils.java

@@ -0,0 +1,25 @@
+package com.contract.util;
+
+import org.springframework.util.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 与 spot 一致:从请求头 {@code lang} 读取语言,未传时默认 zh_CN。
+ */
+public final class RequestUtils {
+
+    private RequestUtils() {
+    }
+
+    public static String getLangFromHeader(HttpServletRequest request) {
+        if (request == null) {
+            return "zh_CN";
+        }
+        String lang = request.getHeader("lang");
+        if (!StringUtils.hasText(lang)) {
+            return "zh_CN";
+        }
+        return lang.trim();
+    }
+}

+ 28 - 0
contract/contract-api/src/main/resources/application.yml

@@ -0,0 +1,28 @@
+server:
+  port: 6028
+spring:
+  datasource:
+    dynamic:
+      primary: kucoin             # 默认数据源
+      strict: false
+      datasource:
+        kucoin:
+          driver-class-name: com.mysql.cj.jdbc.Driver
+          url: jdbc:mysql://172.31.39.212:3306/kucoin?characterEncoding=utf-8&serverTimezone=GMT%2B8&useSSL=false
+          username: kucoin
+          password: KCrbYZ2XcjBtxzdG
+  redis:
+    host: 172.31.39.212
+    port: 6379
+    password: redis123!
+    database: 0
+    timeout: 5000ms
+    lettuce:
+      pool:
+        max-active: 8
+        max-idle: 8
+        min-idle: 1
+        max-wait: 2000
+
+mybatis-plus:
+  mapper-locations: classpath*:mapper/**/*.xml

+ 88 - 0
contract/contract-api/src/main/resources/log4j2-spring.xml

@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<Configuration status="WARN" monitorInterval="30">
+    <Properties>
+        <Property name="app.name">${sys:APP_NAME:-${env:APP_NAME:-application}}</Property>
+        <Property name="log.path">${sys:LOG_PATH:-${env:LOG_PATH:-${sys:user.home}/logs}}</Property>
+        <Property name="log.roll.pattern">${sys:LOG_ROLL_PATTERN:-${env:LOG_ROLL_PATTERN:-yyyy-MM-dd}}</Property>
+        <Property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</Property>
+        <Property name="BYTEPLUS_TLS_ENDPOINT">${env:BYTEPLUS_TLS_ENDPOINT:-${env:VOLCENGINE_ENDPOINT:-}}</Property>
+        <Property name="BYTEPLUS_TLS_ACCESS_KEY_ID">${env:BYTEPLUS_TLS_ACCESS_KEY_ID:-${env:VOLCENGINE_ACCESS_KEY_ID:-}}</Property>
+        <Property name="BYTEPLUS_TLS_ACCESS_KEY_SECRET">${env:BYTEPLUS_TLS_ACCESS_KEY_SECRET:-${env:VOLCENGINE_ACCESS_KEY_SECRET:-}}</Property>
+        <Property name="BYTEPLUS_TLS_REGION">${env:BYTEPLUS_TLS_REGION:-${env:VOLCENGINE_REGION:-ap-southeast-1}}</Property>
+        <Property name="TLS_PROJECT_ID">${sys:byteplus.tls.projectId:-}</Property>
+        <Property name="TLS_TOPIC_ID">${sys:byteplus.tls.topicId:-}</Property>
+        <Property name="BYTEPLUS_TLS_BATCH_SIZE">${env:BYTEPLUS_TLS_BATCH_SIZE:-100}</Property>
+        <Property name="BYTEPLUS_TLS_FLUSH_INTERVAL_SECONDS">${env:BYTEPLUS_TLS_FLUSH_INTERVAL_SECONDS:-5}</Property>
+        <Property name="mybatis.sql.log.level">${sys:MYBATIS_SQL_LOG_LEVEL:-${env:MYBATIS_SQL_LOG_LEVEL:-INFO}}</Property>
+    </Properties>
+
+    <Appenders>
+        <Console name="Console" target="SYSTEM_OUT">
+            <PatternLayout pattern="${LOG_PATTERN}"/>
+        </Console>
+
+        <!-- 按天生成日志,直接写入日期子目录(启动时即创建当天目录,与 logback 行为一致) -->
+        <RollingFile name="RollingLogFile"
+                     filePattern="${log.path}/${app.name}/%d{${log.roll.pattern}}/%d{${log.roll.pattern}}-%i.log">
+            <PatternLayout pattern="${LOG_PATTERN}"/>
+            <Policies>
+                <TimeBasedTriggeringPolicy/>
+                <SizeBasedTriggeringPolicy size="200MB"/>
+            </Policies>
+            <DirectWriteRolloverStrategy maxFiles="10"/>
+        </RollingFile>
+
+        <Async name="AsyncRollingLogFile" bufferSize="262144">
+            <AppenderRef ref="RollingLogFile"/>
+        </Async>
+
+        <BytePlusTLS name="BytePlusTLS"
+                     endpoint="${BYTEPLUS_TLS_ENDPOINT}"
+                     accessKeyId="${BYTEPLUS_TLS_ACCESS_KEY_ID}"
+                     accessKeySecret="${BYTEPLUS_TLS_ACCESS_KEY_SECRET}"
+                     region="${BYTEPLUS_TLS_REGION}"
+                     projectId="${TLS_PROJECT_ID}"
+                     topicId="${TLS_TOPIC_ID}"
+                     batchSize="${BYTEPLUS_TLS_BATCH_SIZE}"
+                     flushIntervalSeconds="${BYTEPLUS_TLS_FLUSH_INTERVAL_SECONDS}">
+            <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
+        </BytePlusTLS>
+    </Appenders>
+
+    <Loggers>
+        <Logger name="org.springframework" level="INFO" additivity="false">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="AsyncRollingLogFile"/>
+        </Logger>
+        <Logger name="com.alibaba.nacos" level="INFO" additivity="false">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="AsyncRollingLogFile"/>
+        </Logger>
+        <Logger name="com.volcengine" level="INFO" additivity="false">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="AsyncRollingLogFile"/>
+        </Logger>
+        <Logger name="com.exchange.log.appender" level="INFO" additivity="false">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="AsyncRollingLogFile"/>
+        </Logger>
+        <Logger name="org.apache.ibatis" level="${mybatis.sql.log.level}" additivity="false">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="AsyncRollingLogFile"/>
+        </Logger>
+        <Logger name="org.apache.ibatis.logging.jdbc" level="${mybatis.sql.log.level}" additivity="false">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="AsyncRollingLogFile"/>
+        </Logger>
+        <Logger name="com.exchange" level="${mybatis.sql.log.level}" additivity="false">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="AsyncRollingLogFile"/>
+            <AppenderRef ref="BytePlusTLS"/>
+        </Logger>
+        <Root level="INFO">
+            <AppenderRef ref="Console"/>
+            <AppenderRef ref="AsyncRollingLogFile"/>
+        </Root>
+    </Loggers>
+</Configuration>

+ 5 - 0
contract/contract-api/src/main/resources/mapper/UserWatchlistMapper.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.contrace.mapper.UserWatchlistMapper">
+</mapper>

+ 28 - 0
contract/contract-entity/pom.xml

@@ -0,0 +1,28 @@
+<?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.contract</groupId>
+        <artifactId>contract</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>contract-entity</artifactId>
+    <packaging>jar</packaging>
+
+    <description>contract 实体模块</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.exchange</groupId>
+            <artifactId>common-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.exchange</groupId>
+            <artifactId>common-mybatisPlus</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 49 - 0
contract/contract-entity/src/main/java/com/contract/entity/AppBanner.java

@@ -0,0 +1,49 @@
+package com.contract.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("app_banner")
+public class AppBanner {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** Banner标题 */
+    private String title;
+
+    /** Banner图片地址 */
+    private String imageUrl;
+
+    /** 跳转类型: internal=APP内部页面, external=外部网页 */
+    private String linkType;
+
+    /** 跳转地址 */
+    private String linkUrl;
+
+    /** 排序值,越大越靠前 */
+    private Integer sort;
+
+    /** 状态: 1=启用, 0=禁用 */
+    private Integer status;
+
+    /** 生效开始时间(为空则不限制) */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime startTime;
+
+    /** 生效结束时间(为空则不限制) */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime endTime;
+
+    @TableField(fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime updateTime;
+}

+ 61 - 0
contract/contract-entity/src/main/java/com/contract/entity/AppHeader.java

@@ -0,0 +1,61 @@
+package com.contract.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("app_header")
+public class AppHeader {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** Header标题 */
+    private String title;
+
+    /** 媒体类型: image=图片, video=视频 */
+    private String mediaType;
+
+    /** 展示类型: 0=通用, 1=日间/夜间 */
+    private Integer displayType;
+
+    /** 通用媒体地址(displayType=0时使用) */
+    private String mediaUrl;
+
+    /** 日间媒体地址(displayType=1时使用) */
+    private String dayMediaUrl;
+
+    /** 夜间媒体地址(displayType=1时使用) */
+    private String nightMediaUrl;
+
+    /** 跳转类型: internal=APP内部页面, external=外部网页 */
+    private String linkType;
+
+    /** 跳转地址 */
+    private String linkUrl;
+
+    /** 排序值,越大越靠前 */
+    private Integer sort;
+
+    /** 状态: 1=启用, 0=禁用 */
+    private Integer status;
+
+    /** 生效开始时间(为空则不限制) */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime startTime;
+
+    /** 生效结束时间(为空则不限制) */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime endTime;
+
+    @TableField(fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime updateTime;
+}

+ 56 - 0
contract/contract-entity/src/main/java/com/contract/entity/CoinExt.java

@@ -0,0 +1,56 @@
+package com.contract.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("coin_ext")
+public class CoinExt {
+
+    /** 主键ID */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 币种,如 BTC */
+    private String symbol;
+
+    /** 中文名称,如 比特币 */
+    private String nameCn;
+
+    /** 英文名称,如 Bitcoin */
+    private String nameEn;
+
+    /** 图标地址 */
+    private String icon;
+
+    /** 排名 */
+    private Integer rank;
+
+    /** 市值 */
+    private String marketCap;
+
+    /** 流通供应量 */
+    private String circulatingSupply;
+
+    /** 发行价(USD) */
+    private BigDecimal issuePrice;
+
+    /** 历史最高价(USD) */
+    private BigDecimal athPrice;
+
+    /** 历史最高价日期 */
+    private LocalDate athDate;
+
+    /** 白皮书地址 */
+    private String whitepaper;
+
+    /** 创建时间 */
+    private LocalDateTime createdTime;
+
+    /** 更新时间 */
+    private LocalDateTime updatedTime;
+}

+ 36 - 0
contract/contract-entity/src/main/java/com/contract/entity/CurrencyCoin.java

@@ -0,0 +1,36 @@
+package com.contract.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("currency_coin")
+public class CurrencyCoin {
+
+    /** 主键ID */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 法币名称 */
+    private String currency;
+
+    /** 法币符号(¥) */
+    private String symbol;
+
+    /** 当前usdt汇率 */
+    private BigDecimal rate;
+
+    /** 精度 */
+    private Integer coinPrecision;
+
+    /** 创建时间 */
+    @TableField("ctime")
+    private LocalDateTime ctime;
+
+    /** 更新时间 */
+    @TableField("mtime")
+    private LocalDateTime mtime;
+}

+ 33 - 0
contract/contract-entity/src/main/java/com/contract/entity/NodeInfo.java

@@ -0,0 +1,33 @@
+package com.contract.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("node_info")
+public class NodeInfo {
+
+    /** 主键 */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 名称 */
+    private String name;
+
+    /** api地址 */
+    private String apiHost;
+
+    /** ws地址 */
+    private String wsHost;
+
+    /** 状态 1开启 2关闭 */
+    private Integer status;
+
+    /** 创建时间 */
+    private LocalDateTime createdTime;
+
+    /** 修改时间 */
+    private LocalDateTime updatedTime;
+}

+ 26 - 0
contract/contract-entity/src/main/java/com/contract/entity/UserWatchlist.java

@@ -0,0 +1,26 @@
+package com.contract.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("user_watchlist")
+public class UserWatchlist {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long memberId;
+
+    private String symbol;
+
+    private Integer status;
+
+    private Integer sort;
+
+    private LocalDateTime createdTime;
+
+    private LocalDateTime updatedTime;
+}

+ 115 - 0
contract/contract-entity/src/main/java/com/contract/entity/resp/MessageResult.java

@@ -0,0 +1,115 @@
+package com.contract.entity.resp;
+
+
+import com.alibaba.fastjson2.JSONObject;
+import com.contract.enums.MessageCode;
+
+public class MessageResult<T> {
+	private T data;
+	public MessageResult(int code , String msg){
+		this.code = code;
+		this.message = msg;
+	}
+	public MessageResult(int code , String msg, T object){
+		this.code = code;
+		this.message = msg;
+		this.data = object;
+	}
+	public MessageResult() {
+		// TODO Auto-generated constructor stub
+	}
+	
+	public MessageResult(JSONObject json) {
+		this.code = json.getInteger("code");
+		this.message = json.getString("message");
+		if(json.containsKey("data"))
+			this.setData((T)json.get("data"));
+	}
+	
+	public static MessageResult success(){
+		return new MessageResult(0, MessageCode.SUCCESS.getMessage(null));
+	}
+	public static MessageResult success(String msg){
+		return new MessageResult(0,msg);
+	}
+	public static MessageResult success(String msg,Object data){
+		return new MessageResult(0,msg,data);
+	}
+
+	public static MessageResult successLang(String lang) {
+		return new MessageResult(0, MessageCode.SUCCESS.getMessage(lang));
+	}
+
+	public static MessageResult successLang(String lang, Object data) {
+		return new MessageResult(0, MessageCode.SUCCESS.getMessage(lang), data);
+	}
+
+	public static MessageResult failLang(String lang, MessageCode code) {
+		return new MessageResult(code.getCode(), code.getMessage(lang));
+	}
+
+	public static MessageResult failLang(String lang, MessageCode code, String message) {
+		String msg = (message != null && !message.isEmpty()) ? message : code.getMessage(lang);
+		return new MessageResult(code.getCode(), msg);
+	}
+	
+	public static MessageResult error(int code,String msg){
+		return new MessageResult(code,msg);
+	}
+	public static MessageResult error(String msg){
+		return new MessageResult(500,msg);
+	}
+	
+	private int code;
+	private String message;
+	
+	private String totalPage;
+	private String totalElement;
+
+    public String getTotalPage() {
+        return totalPage;
+    }
+
+    public void setTotalPage(String totalPage) {
+        this.totalPage = totalPage;
+    }
+
+    public String getTotalElement() {
+        return totalElement;
+    }
+
+    public void setTotalElement(String totalElement) {
+        this.totalElement = totalElement;
+    }
+
+    public int getCode() {
+		return code;
+	}
+	public void setCode(int code) {
+		this.code = code;
+	}
+	public String getMessage() {
+		return message;
+	}
+	public void setMessage(String message) {
+		this.message = message;
+	}
+	
+	@Override
+    public String toString(){
+		return JSONObject.toJSONString(this);
+		//return "{\"code\":"+code+",\"message\":\""+message+"\"}";
+	}
+	public Object getData() {
+		return data;
+	}
+	public void setData(T data) {
+		this.data = data;
+	}
+
+	public static MessageResult getSuccessInstance(String message ,Object data){
+		MessageResult result = success(message);
+		result.setData(data);
+		return result ;
+	}
+}

+ 16 - 0
contract/contract-entity/src/main/java/com/contract/entity/transform/AuthMember.java

@@ -0,0 +1,16 @@
+package com.contract.entity.transform;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class AuthMember implements Serializable {
+    private static final long serialVersionUID = -4199550203850153635L;
+    private long id;
+    private String name;
+    private String realName;
+    private String mobilePhone;
+    private String email;
+    private String authToken;
+}

+ 22 - 0
contract/contract-entity/src/main/java/com/contract/enums/I18nLangKeys.java

@@ -0,0 +1,22 @@
+package com.contract.enums;
+
+import java.util.Locale;
+
+/**
+ * 与 spot 一致:语言 Map 键为去掉 {@code -}、{@code _} 后大写(如 ZHCN、ENUS)。
+ */
+public final class I18nLangKeys {
+
+    public static final String DEFAULT = "ZHCN";
+
+    private I18nLangKeys() {
+    }
+
+    public static String normalize(String lang) {
+        if (lang == null || lang.isBlank()) {
+            return DEFAULT;
+        }
+        String compact = lang.trim().replace("-", "").replace("_", "").toUpperCase(Locale.ROOT);
+        return compact.isEmpty() ? DEFAULT : compact;
+    }
+}

+ 55 - 0
contract/contract-entity/src/main/java/com/contract/enums/MessageCode.java

@@ -0,0 +1,55 @@
+package com.contract.enums;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 合约 API 文案枚举(与 spot 相同的 9 语与 {@link I18nLangKeys} 键规则)。
+ */
+public enum MessageCode {
+
+    SUCCESS(0,
+            "成功", "成功", "Success",
+            "성공", "成功", "Erfolg",
+            "Succès", "Successo", "Éxito"),
+
+    BANNER_NOT_FOUND(500,
+            "Banner不存在", "輪播圖不存在", "Banner not found",
+            "배너를 찾을 수 없습니다", "バナーが見つかりません", "Banner nicht gefunden",
+            "Bannière introuvable", "Banner non trovato", "Banner no encontrado"),
+
+    HEADER_NOT_FOUND(500,
+            "Header不存在", "頁首不存在", "Header not found",
+            "헤더를 찾을 수 없습니다", "ヘッダーが見つかりません", "Header nicht gefunden",
+            "En-tête introuvable", "Header non trovato", "Encabezado no encontrado");
+
+    private final int code;
+    private final Map<String, String> messages;
+
+    MessageCode(int code,
+                String zhCN, String zhHK, String enUS,
+                String koKR, String jaJP, String deDE,
+                String frFR, String itIT, String esES) {
+        this.code = code;
+        this.messages = new HashMap<>(16);
+        this.messages.put("ZHCN", zhCN);
+        this.messages.put("ZHHK", zhHK);
+        this.messages.put("ENUS", enUS);
+        this.messages.put("KOKR", koKR);
+        this.messages.put("JAJP", jaJP);
+        this.messages.put("DEDE", deDE);
+        this.messages.put("FRFR", frFR);
+        this.messages.put("ITIT", itIT);
+        this.messages.put("ESES", esES);
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getMessage(String lang) {
+        String key = I18nLangKeys.normalize(lang);
+        String msg = messages.get(key);
+        return msg != null ? msg : messages.get(I18nLangKeys.DEFAULT);
+    }
+}

+ 149 - 0
contract/doc/sys_help_category_design.md

@@ -0,0 +1,149 @@
+# 帮助文章分类枚举表设计
+
+## 背景
+
+当前 `SysHelpClassification` 是 Java 硬编码枚举,新增分类需要改代码重新部署。
+改为数据库字典表后,管理后台直接维护分类,各业务模块通过 `category_code` 查询对应内容,无需改代码。
+
+---
+
+## 一、表结构设计
+
+### 1. 分类主表 `sys_help_category`
+
+存储所有分类/协议类型,管理后台从此表读取下拉选项。
+
+```sql
+CREATE TABLE `sys_help_category` (
+  `id`          BIGINT       NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `code`        VARCHAR(50)  NOT NULL                COMMENT '分类编码(英文唯一标识,业务方使用此字段查询)',
+  `name_zh`     VARCHAR(100) NOT NULL                COMMENT '中文名称',
+  `name_en`     VARCHAR(100) NOT NULL                COMMENT '英文名称',
+  `scene`       VARCHAR(50)  NOT NULL DEFAULT 'HELP' COMMENT '适用场景: HELP=帮助中心, PROTOCOL=协议展示, NOTICE=公告',
+  `sort`        INT          NOT NULL DEFAULT 0       COMMENT '排序值,越大越靠前',
+  `status`      TINYINT      NOT NULL DEFAULT 1       COMMENT '状态: 1=启用, 0=禁用',
+  `remark`      VARCHAR(255)          DEFAULT NULL   COMMENT '备注(说明该分类的用途)',
+  `create_time` DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_code` (`code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='帮助文章分类枚举表';
+```
+
+### 2. `sys_help` 表调整
+
+将原来的硬编码枚举字段替换为分类 code 外键。
+
+```sql
+-- 新增 category_code 字段,替代原 sys_help_classification 枚举字段
+ALTER TABLE `sys_help`
+  ADD COLUMN `category_code` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '分类编码,关联 sys_help_category.code' AFTER `title`,
+  ADD INDEX `idx_category_code` (`category_code`);
+```
+
+> **迁移说明**:上线时先写入初始数据(见下方),再执行字段迁移,将旧枚举 ordinal 映射到对应 code。
+
+---
+
+## 二、初始数据
+
+对应现有 `SysHelpClassification` 枚举,全量初始化:
+
+```sql
+INSERT INTO `sys_help_category` (`code`, `name_zh`, `name_en`, `scene`, `sort`, `status`, `remark`) VALUES
+('HELP',            '新手入门',   'Getting Started',   'HELP',     100, 1, '注册、下载、基础操作等新手引导'),
+('FAQ',             '常见问题',   'FAQ',               'HELP',     90,  1, '用户高频问题汇总'),
+('EXCHANGE',        '交易指南',   'Trading Guide',     'HELP',     80,  1, '现货/合约交易操作说明'),
+('COININFO',        '币种资料',   'Coin Info',         'HELP',     70,  1, '各币种项目介绍'),
+('ANALYSIS',        '行情技术',   'Market Analysis',   'HELP',     60,  1, '技术分析、行情解读'),
+('FOLLOW_PROTOCOL', '交易员条款', 'Trader Agreement',  'PROTOCOL', 55,  1, '带单交易员服务协议'),
+('PRIVACY',         '隐私条款',   'Privacy Policy',    'PROTOCOL', 50,  1, '用户隐私政策'),
+('DISCLAIMER',      '免责声明',   'Disclaimer',        'PROTOCOL', 45,  1, '平台免责声明'),
+('PROTOCOL',        '条款协议',   'Terms of Service',  'PROTOCOL', 40,  1, '用户服务协议/利用规约'),
+('QR_CODE',         'APP二维码',  'APP QR Code',       'HELP',     10,  1, '应用下载二维码展示页'),
+('OTHER',           '其他',       'Other',             'HELP',     0,   1, '未归类内容');
+```
+
+---
+
+## 三、字段说明
+
+### `sys_help_category` 字段
+
+| 字段        | 类型         | 必填 | 说明                                                              |
+|-------------|--------------|------|-------------------------------------------------------------------|
+| id          | bigint       | 是   | 主键,自增                                                        |
+| code        | varchar(50)  | 是   | 唯一编码,**业务方通过此字段查询**,如 `PROTOCOL`、`PRIVACY`    |
+| name_zh     | varchar(100) | 是   | 中文名称,管理后台下拉显示                                        |
+| name_en     | varchar(100) | 是   | 英文名称,前端多语言使用                                          |
+| scene       | varchar(50)  | 是   | 适用场景,用于前端分区展示,见场景枚举说明                        |
+| sort        | int          | 是   | 排序值,越大越靠前,默认 0                                        |
+| status      | tinyint      | 是   | 1=启用,0=禁用;禁用后该分类不再出现在下拉中                     |
+| remark      | varchar(255) | 否   | 备注,说明该分类的业务用途                                        |
+| create_time | datetime     | 是   | 创建时间                                                          |
+| update_time | datetime     | 是   | 最后修改时间                                                      |
+
+### `scene` 场景枚举值
+
+| 值       | 说明                                         |
+|----------|----------------------------------------------|
+| HELP     | 帮助中心展示(新手指南、FAQ、交易指南等)    |
+| PROTOCOL | 协议页面展示(服务条款、隐私政策、免责等)   |
+| NOTICE   | 公告/通知(预留,后续扩展)                  |
+
+---
+
+## 四、业务查询示例
+
+### 查询帮助中心所有启用分类(供下拉选择)
+```sql
+SELECT id, code, name_zh, name_en
+FROM sys_help_category
+WHERE status = 1 AND scene = 'HELP'
+ORDER BY sort DESC;
+```
+
+### 查询指定协议类型的文章(如服务条款,支持多语言)
+```sql
+SELECT h.id, h.title, h.content, h.lang
+FROM sys_help h
+WHERE h.category_code = 'PROTOCOL'
+  AND h.status = 1       -- CommonStatus.NORMAL
+  AND h.lang = 'EN'
+ORDER BY h.is_top ASC, h.sort DESC;
+```
+
+### 查询协议页所有分类及其文章(一次性加载)
+```sql
+SELECT c.code, c.name_zh, h.title, h.content, h.lang
+FROM sys_help_category c
+LEFT JOIN sys_help h ON h.category_code = c.code AND h.status = 1
+WHERE c.scene = 'PROTOCOL' AND c.status = 1
+ORDER BY c.sort DESC, h.sort DESC;
+```
+
+---
+
+## 五、后续扩展方式
+
+新增分类时,**只需在管理后台插入一行**,无需改代码:
+
+```sql
+-- 示例:新增"新手指南"分类(APP端新手流程)
+INSERT INTO `sys_help_category` (`code`, `name_zh`, `name_en`, `scene`, `sort`, `remark`)
+VALUES ('BEGINNER_GUIDE', '新手指南', 'Beginner Guide', 'HELP', 95, 'APP端新手引导流程专用');
+```
+
+前端/API 按 `code` 字段拉取对应分类的文章,`code` 值与业务约定后保持不变,新增分类不影响现有业务。
+
+---
+
+## 六、管理后台接口建议
+
+| 接口 | 方法 | 路径 | 说明 |
+|------|------|------|------|
+| 分类列表(下拉用) | GET | `/cms/help-category/list` | 返回所有启用分类,按 scene 过滤 |
+| 分类分页查询 | POST | `/cms/help-category/page-query` | 管理列表,支持 scene/status 筛选 |
+| 新增分类 | POST | `/cms/help-category/create` | 新增一个分类 |
+| 修改分类 | POST | `/cms/help-category/update` | 修改名称、排序、状态等 |
+| 删除分类 | POST | `/cms/help-category/deletes` | 软删除(status=0)或物理删除 |

+ 105 - 0
contract/pom.xml

@@ -0,0 +1,105 @@
+<?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>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.7.18</version>
+        <relativePath/>
+    </parent>
+
+    <groupId>com.contract</groupId>
+    <artifactId>contract</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>pom</packaging>
+
+    <description>contract 合约交易系统</description>
+
+    <modules>
+        <module>contract-entity</module>
+        <module>contract-api</module>
+    </modules>
+
+    <properties>
+        <java.version>17</java.version>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <spring-cloud.version>2021.0.8</spring-cloud.version>
+        <spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
+        <common.version>1.0.0-SNAPSHOT</common.version>
+        <lombok.version>1.18.30</lombok.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <!-- Spring Cloud -->
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-dependencies</artifactId>
+                <version>${spring-cloud.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <!-- Spring Cloud Alibaba -->
+            <dependency>
+                <groupId>com.alibaba.cloud</groupId>
+                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
+                <version>${spring-cloud-alibaba.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <!-- Common 包版本管理 -->
+            <dependency>
+                <groupId>com.exchange</groupId>
+                <artifactId>common-core</artifactId>
+                <version>${common.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.exchange</groupId>
+                <artifactId>common-log</artifactId>
+                <version>${common.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.exchange</groupId>
+                <artifactId>common-redis</artifactId>
+                <version>${common.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.exchange</groupId>
+                <artifactId>common-mybatisPlus</artifactId>
+                <version>${common.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.exchange</groupId>
+                <artifactId>common-utils</artifactId>
+                <version>${common.version}</version>
+            </dependency>
+            <!-- 多数据源 -->
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
+                <version>3.6.1</version>
+            </dependency>
+            <!-- 子模块版本管理 -->
+            <dependency>
+                <groupId>com.contract</groupId>
+                <artifactId>contract-entity</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <!-- Lombok 所有模块共享 -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>${lombok.version}</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+</project>