From 7694a34adea574baa19aee928d8d87695bb92922 Mon Sep 17 00:00:00 2001 From: sion Date: Sat, 21 Mar 2026 20:52:33 +0800 Subject: [PATCH] =?UTF-8?q?Initial=20commit:=20Monisuo=20-=20=E8=99=9A?= =?UTF-8?q?=E6=8B=9F=E8=B4=A7=E5=B8=81=E6=A8=A1=E6=8B=9F=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 功能模块: - 用户注册/登录/KYC - 资金账户/交易账户 - 实时行情/币种管理 - 即时交易/充提审核 - 管理后台 技术栈: - 后端: SpringBoot 2.2.4 + MyBatis Plus - 前端: uni-app x (Vue3 + UTS) - 数据库: MySQL Co-Authored-By: Claude Opus 4.6 --- .agent/skills/README.md | 48 ++ .../skills/genre-knowledge/fantasy/SKILL.md | 390 ++++++++++++ .../skills/genre-knowledge/mystery/SKILL.md | 374 +++++++++++ .../skills/genre-knowledge/romance/SKILL.md | 244 +++++++ .agent/skills/novelweave-workflow/SKILL.md | 185 ++++++ .../consistency-checker/SKILL.md | 367 +++++++++++ .../forgotten-elements/SKILL.md | 148 +++++ .../getting-started/SKILL.md | 225 +++++++ .../pre-write-checklist/SKILL.md | 549 ++++++++++++++++ .../CONFLICT_RESOLUTION.md | 131 ++++ .../requirement-detector/EXAMPLES.md | 153 +++++ .../requirement-detector/KEYWORDS.md | 178 ++++++ .../requirement-detector/SKILL.md | 157 +++++ .../setting-detector/SKILL.md | 597 ++++++++++++++++++ .../style-detector/CONFLICT_RESOLUTION.md | 140 ++++ .../style-detector/EXAMPLES.md | 218 +++++++ .../style-detector/KEYWORDS.md | 227 +++++++ .../quality-assurance/style-detector/SKILL.md | 131 ++++ .../quality-assurance/workflow-guide/SKILL.md | 420 ++++++++++++ .../dialogue-techniques/SKILL.md | 389 ++++++++++++ .../scene-structure/SKILL.md | 397 ++++++++++++ .gitignore | 48 ++ app/App.uvue | 60 ++ app/PACKAGING.md | 176 ++++++ app/README.md | 203 ++++++ app/api/asset.uts | 43 ++ app/api/fund.uts | 36 ++ app/api/index.uts | 9 + app/api/market.uts | 25 + app/api/request.uts | 130 ++++ app/api/trade.uts | 39 ++ app/api/user.uts | 39 ++ app/index.html | 21 + app/main.uts | 15 + app/manifest.json | 52 ++ app/package.json | 39 ++ app/pages.json | 106 ++++ app/pages/asset/asset.uvue | 448 +++++++++++++ app/pages/index/index.uvue | 305 +++++++++ app/pages/login/login.uvue | 169 +++++ app/pages/market/market.uvue | 300 +++++++++ app/pages/mine/mine.uvue | 209 ++++++ app/pages/register/register.uvue | 203 ++++++ app/pages/trade/trade.uvue | 361 +++++++++++ app/static/default-avatar.png | 2 + app/static/logo.png | 2 + app/static/tabbar/asset-active.png | 2 + app/static/tabbar/asset.png | 2 + app/static/tabbar/home-active.png | 2 + app/static/tabbar/home.png | 2 + app/static/tabbar/market-active.png | 2 + app/static/tabbar/market.png | 2 + app/static/tabbar/mine-active.png | 2 + app/static/tabbar/mine.png | 2 + app/static/tabbar/trade-active.png | 2 + app/static/tabbar/trade.png | 2 + app/uni.scss | 54 ++ app/vite.config.ts | 31 + pom.xml | 145 +++++ sql/init.sql | 257 ++++++++ .../com/it/rattan/SpcCloudApplication.java | 23 + .../java/com/it/rattan/SwaggerConfig.java | 62 ++ .../it/rattan/config/RattanClientConfig.java | 13 + .../java/com/it/rattan/config/WebConfig.java | 40 ++ .../java/com/it/rattan/enums/RattanMark.java | 9 + .../exception/GlobExceptionHandler.java | 17 + .../it/rattan/exception/LoginException.java | 21 + .../it/rattan/intceptor/MySqlInterceptor.java | 103 +++ .../it/rattan/monisuo/common/PageResult.java | 40 ++ .../com/it/rattan/monisuo/common/Result.java | 64 ++ .../rattan/monisuo/context/UserContext.java | 46 ++ .../monisuo/controller/AdminController.java | 359 +++++++++++ .../monisuo/controller/AssetController.java | 129 ++++ .../monisuo/controller/FundController.java | 121 ++++ .../monisuo/controller/MarketController.java | 74 +++ .../monisuo/controller/TradeController.java | 134 ++++ .../monisuo/controller/UserController.java | 120 ++++ .../it/rattan/monisuo/entity/AccountFlow.java | 51 ++ .../it/rattan/monisuo/entity/AccountFund.java | 43 ++ .../rattan/monisuo/entity/AccountTrade.java | 49 ++ .../com/it/rattan/monisuo/entity/Admin.java | 61 ++ .../com/it/rattan/monisuo/entity/Coin.java | 91 +++ .../it/rattan/monisuo/entity/OrderFund.java | 64 ++ .../it/rattan/monisuo/entity/OrderTrade.java | 61 ++ .../com/it/rattan/monisuo/entity/User.java | 68 ++ .../it/rattan/monisuo/filter/TokenFilter.java | 86 +++ .../monisuo/mapper/AccountFlowMapper.java | 12 + .../monisuo/mapper/AccountFundMapper.java | 26 + .../monisuo/mapper/AccountTradeMapper.java | 19 + .../it/rattan/monisuo/mapper/AdminMapper.java | 12 + .../it/rattan/monisuo/mapper/CoinMapper.java | 12 + .../monisuo/mapper/OrderFundMapper.java | 23 + .../monisuo/mapper/OrderTradeMapper.java | 12 + .../it/rattan/monisuo/mapper/UserMapper.java | 12 + .../rattan/monisuo/service/AssetService.java | 256 ++++++++ .../rattan/monisuo/service/CoinService.java | 58 ++ .../rattan/monisuo/service/FundService.java | 244 +++++++ .../rattan/monisuo/service/TradeService.java | 189 ++++++ .../rattan/monisuo/service/UserService.java | 156 +++++ .../com/it/rattan/monisuo/util/JwtUtil.java | 103 +++ .../it/rattan/monisuo/util/OrderNoUtil.java | 46 ++ .../java/com/it/rattan/rpc/BaseResponse.java | 41 ++ .../com/it/rattan/rpc/ObjectRestResponse.java | 31 + .../com/it/rattan/rpc/RattanResponse.java | 35 + src/main/resources/application-dev.yml | 33 + src/main/resources/application-prd.yml | 58 ++ src/main/resources/application.yml | 5 + src/main/resources/logback-spring.xml | 16 + 108 files changed, 12563 insertions(+) create mode 100644 .agent/skills/README.md create mode 100644 .agent/skills/genre-knowledge/fantasy/SKILL.md create mode 100644 .agent/skills/genre-knowledge/mystery/SKILL.md create mode 100644 .agent/skills/genre-knowledge/romance/SKILL.md create mode 100644 .agent/skills/novelweave-workflow/SKILL.md create mode 100644 .agent/skills/quality-assurance/consistency-checker/SKILL.md create mode 100644 .agent/skills/quality-assurance/forgotten-elements/SKILL.md create mode 100644 .agent/skills/quality-assurance/getting-started/SKILL.md create mode 100644 .agent/skills/quality-assurance/pre-write-checklist/SKILL.md create mode 100644 .agent/skills/quality-assurance/requirement-detector/CONFLICT_RESOLUTION.md create mode 100644 .agent/skills/quality-assurance/requirement-detector/EXAMPLES.md create mode 100644 .agent/skills/quality-assurance/requirement-detector/KEYWORDS.md create mode 100644 .agent/skills/quality-assurance/requirement-detector/SKILL.md create mode 100644 .agent/skills/quality-assurance/setting-detector/SKILL.md create mode 100644 .agent/skills/quality-assurance/style-detector/CONFLICT_RESOLUTION.md create mode 100644 .agent/skills/quality-assurance/style-detector/EXAMPLES.md create mode 100644 .agent/skills/quality-assurance/style-detector/KEYWORDS.md create mode 100644 .agent/skills/quality-assurance/style-detector/SKILL.md create mode 100644 .agent/skills/quality-assurance/workflow-guide/SKILL.md create mode 100644 .agent/skills/writing-techniques/dialogue-techniques/SKILL.md create mode 100644 .agent/skills/writing-techniques/scene-structure/SKILL.md create mode 100644 .gitignore create mode 100644 app/App.uvue create mode 100644 app/PACKAGING.md create mode 100644 app/README.md create mode 100644 app/api/asset.uts create mode 100644 app/api/fund.uts create mode 100644 app/api/index.uts create mode 100644 app/api/market.uts create mode 100644 app/api/request.uts create mode 100644 app/api/trade.uts create mode 100644 app/api/user.uts create mode 100644 app/index.html create mode 100644 app/main.uts create mode 100644 app/manifest.json create mode 100644 app/package.json create mode 100644 app/pages.json create mode 100644 app/pages/asset/asset.uvue create mode 100644 app/pages/index/index.uvue create mode 100644 app/pages/login/login.uvue create mode 100644 app/pages/market/market.uvue create mode 100644 app/pages/mine/mine.uvue create mode 100644 app/pages/register/register.uvue create mode 100644 app/pages/trade/trade.uvue create mode 100644 app/static/default-avatar.png create mode 100644 app/static/logo.png create mode 100644 app/static/tabbar/asset-active.png create mode 100644 app/static/tabbar/asset.png create mode 100644 app/static/tabbar/home-active.png create mode 100644 app/static/tabbar/home.png create mode 100644 app/static/tabbar/market-active.png create mode 100644 app/static/tabbar/market.png create mode 100644 app/static/tabbar/mine-active.png create mode 100644 app/static/tabbar/mine.png create mode 100644 app/static/tabbar/trade-active.png create mode 100644 app/static/tabbar/trade.png create mode 100644 app/uni.scss create mode 100644 app/vite.config.ts create mode 100644 pom.xml create mode 100644 sql/init.sql create mode 100644 src/main/java/com/it/rattan/SpcCloudApplication.java create mode 100644 src/main/java/com/it/rattan/SwaggerConfig.java create mode 100644 src/main/java/com/it/rattan/config/RattanClientConfig.java create mode 100644 src/main/java/com/it/rattan/config/WebConfig.java create mode 100644 src/main/java/com/it/rattan/enums/RattanMark.java create mode 100644 src/main/java/com/it/rattan/exception/GlobExceptionHandler.java create mode 100644 src/main/java/com/it/rattan/exception/LoginException.java create mode 100644 src/main/java/com/it/rattan/intceptor/MySqlInterceptor.java create mode 100644 src/main/java/com/it/rattan/monisuo/common/PageResult.java create mode 100644 src/main/java/com/it/rattan/monisuo/common/Result.java create mode 100644 src/main/java/com/it/rattan/monisuo/context/UserContext.java create mode 100644 src/main/java/com/it/rattan/monisuo/controller/AdminController.java create mode 100644 src/main/java/com/it/rattan/monisuo/controller/AssetController.java create mode 100644 src/main/java/com/it/rattan/monisuo/controller/FundController.java create mode 100644 src/main/java/com/it/rattan/monisuo/controller/MarketController.java create mode 100644 src/main/java/com/it/rattan/monisuo/controller/TradeController.java create mode 100644 src/main/java/com/it/rattan/monisuo/controller/UserController.java create mode 100644 src/main/java/com/it/rattan/monisuo/entity/AccountFlow.java create mode 100644 src/main/java/com/it/rattan/monisuo/entity/AccountFund.java create mode 100644 src/main/java/com/it/rattan/monisuo/entity/AccountTrade.java create mode 100644 src/main/java/com/it/rattan/monisuo/entity/Admin.java create mode 100644 src/main/java/com/it/rattan/monisuo/entity/Coin.java create mode 100644 src/main/java/com/it/rattan/monisuo/entity/OrderFund.java create mode 100644 src/main/java/com/it/rattan/monisuo/entity/OrderTrade.java create mode 100644 src/main/java/com/it/rattan/monisuo/entity/User.java create mode 100644 src/main/java/com/it/rattan/monisuo/filter/TokenFilter.java create mode 100644 src/main/java/com/it/rattan/monisuo/mapper/AccountFlowMapper.java create mode 100644 src/main/java/com/it/rattan/monisuo/mapper/AccountFundMapper.java create mode 100644 src/main/java/com/it/rattan/monisuo/mapper/AccountTradeMapper.java create mode 100644 src/main/java/com/it/rattan/monisuo/mapper/AdminMapper.java create mode 100644 src/main/java/com/it/rattan/monisuo/mapper/CoinMapper.java create mode 100644 src/main/java/com/it/rattan/monisuo/mapper/OrderFundMapper.java create mode 100644 src/main/java/com/it/rattan/monisuo/mapper/OrderTradeMapper.java create mode 100644 src/main/java/com/it/rattan/monisuo/mapper/UserMapper.java create mode 100644 src/main/java/com/it/rattan/monisuo/service/AssetService.java create mode 100644 src/main/java/com/it/rattan/monisuo/service/CoinService.java create mode 100644 src/main/java/com/it/rattan/monisuo/service/FundService.java create mode 100644 src/main/java/com/it/rattan/monisuo/service/TradeService.java create mode 100644 src/main/java/com/it/rattan/monisuo/service/UserService.java create mode 100644 src/main/java/com/it/rattan/monisuo/util/JwtUtil.java create mode 100644 src/main/java/com/it/rattan/monisuo/util/OrderNoUtil.java create mode 100644 src/main/java/com/it/rattan/rpc/BaseResponse.java create mode 100644 src/main/java/com/it/rattan/rpc/ObjectRestResponse.java create mode 100644 src/main/java/com/it/rattan/rpc/RattanResponse.java create mode 100644 src/main/resources/application-dev.yml create mode 100644 src/main/resources/application-prd.yml create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/logback-spring.xml diff --git a/.agent/skills/README.md b/.agent/skills/README.md new file mode 100644 index 0000000..b8bd572 --- /dev/null +++ b/.agent/skills/README.md @@ -0,0 +1,48 @@ +# NovelWeave Agent Skills + +这个目录包含您的项目 Agent Skills。 + +## 说明 + +- ✅ 您可以自由修改、删除或添加 Skills +- ✅ Skills 文件会被 Git 跟踪,团队共享 +- ✅ 每个 Skill 是一个目录,包含 SKILL.md 文件 + +## 目录结构 + +``` +.agent/skills/ +├── genre-knowledge/ # 类型知识 +│ ├── romance/ +│ ├── mystery/ +│ └── fantasy/ +├── quality-assurance/ # 质量保证 +│ ├── consistency-checker/ +│ └── novelweave-workflow/ +└── writing-techniques/ # 写作技巧 + ├── dialogue-techniques/ + └── scene-structure/ +``` + +## 如何使用 + +1. **查看 Skill**:打开任意 Skill 目录中的 SKILL.md 文件 +2. **修改 Skill**:直接编辑 SKILL.md 或添加支持文件 +3. **删除 Skill**:删除整个 Skill 目录 +4. **添加新 Skill**:创建新目录,添加 SKILL.md 文件 + +## 团队协作 + +1. 修改 Skills 后提交到 Git +2. 团队成员 pull 后自动获得更新 +3. 冲突时手动解决(像普通代码一样) + +## 参考资料 + +- [Skills 编写指南](https://docs.novelweave.com/skills-guide) +- [SKILL.md 格式规范](https://docs.novelweave.com/skill-format) + +--- + +_初始化时间:2026-03-16T14:21:22.728Z_ +_NovelWeave 版本:0.16.0_ diff --git a/.agent/skills/genre-knowledge/fantasy/SKILL.md b/.agent/skills/genre-knowledge/fantasy/SKILL.md new file mode 100644 index 0000000..e50c35e --- /dev/null +++ b/.agent/skills/genre-knowledge/fantasy/SKILL.md @@ -0,0 +1,390 @@ +--- +name: fantasy-world-building +description: "当用户提到奇幻、魔法系统或世界构建时使用 - 提供奇幻类型规范、魔法系统设计模式和世界构建框架" +allowed-tools: Read, Grep +--- + +# 奇幻小说世界构建规范 + +## 快速参考 + +| 元素 | 指导原则 | 关键点 | +| ------------- | -------------- | ------------------ | +| **魔法系统** | 必须有清晰规则 | 限制比力量更重要 | +| **世界设定** | 内在自洽 | 每个规则都有原因 | +| **种族/生物** | 独特且有逻辑 | 避免单纯的人类翻版 | +| **历史深度** | 至少三代历史 | 过去影响现在 | +| **政治结构** | 权力分布清晰 | 冲突有根源 | + +## 核心原则 + +### Sanderson 魔法法则 + +**第一法则**:读者从魔法中获得的满足感,与其理解魔法的程度成正比 + +- 如果魔法要解决问题,读者必须理解它的规则 +- 软魔法系统(神秘)用于氛围和奇观 +- 硬魔法系统(规则明确)用于解决问题 + +**第二法则**:限制比力量更有趣 + +- 强大的魔法需要强大的代价 +- 限制创造冲突和策略 +- 完美的系统无法创造戏剧 + +**第三法则**:在添加新东西之前,先扩展已有的 + +- 深化现有元素胜过添加新元素 +- 相互关联的系统比孤立的更强 +- 复杂度应该有机生长 + +## 魔法系统设计 + +### 硬魔法系统(规则明确) + +**必要元素**: + +1. **能量来源** + - 魔法从哪里来? + - 是有限的还是无限的? + - 可以耗尽吗? + +2. **使用规则** + - 谁能使用?如何获得? + - 需要什么条件? + - 有什么限制? + +3. **代价/后果** + - 使用魔法的成本是什么? + - 过度使用会怎样? + - 是否有长期影响? + +4. **可能与不可能** + - 魔法能做什么? + - 明确不能做什么 + - 边界在哪里? + +**示例框架**: + +``` +魔法系统:元素操控 +来源:每个人出生时拥有一种元素亲和 +规则:只能操控自己的元素,需要该元素存在于周围 +限制:精神疲劳,过度使用导致元素反噬 +代价:使用魔法时消耗生命力,需要休息恢复 +禁忌:不能创造元素,只能操控;不能操控生物体内的元素 +``` + +### 软魔法系统(神秘感) + +**特征**: + +- 规则不完全为读者所知 +- 更多用于氛围和奇观 +- 不应该方便地解决主要冲突 +- 保持神秘感和敬畏 + +**使用场景**: + +- 古老的魔法,已部分遗失 +- 神祇力量,超越凡人理解 +- 背景奇观和世界建设 +- 不影响主线解决的辅助情节 + +## 世界构建框架 + +### 地理与环境 + +**必须考虑**: + +1. **地形** + - 山脉、河流、海洋如何影响文明? + - 气候如何塑造文化? + - 资源分布如何影响经济? + +2. **生态系统** + - 食物链是什么样的? + - 魔法生物如何适应环境? + - 人类/其他智慧种族如何生存? + +3. **魔法对地理的影响** + - 魔法如何改变自然? + - 有魔法造成的地标吗? + - 魔法灾难留下了什么痕迹? + +### 社会与文化 + +**层次架构**: + +**政治层面**: + +- 政府形式(君主制、民主、神权等) +- 权力如何传递? +- 魔法如何影响政治? +- 不同派系的冲突 + +**经济层面**: + +- 贸易什么?为什么? +- 货币系统 +- 魔法如何影响经济? +- 资源稀缺性 + +**社会层面**: + +- 阶级结构 +- 种族关系 +- 魔法使用者的地位 +- 教育和知识传播 + +**文化层面**: + +- 宗教和信仰 +- 艺术和娱乐 +- 节日和传统 +- 价值观和禁忌 + +### 历史深度 + +**最少三代历史**: + +**当代**(故事发生时): + +- 现状是什么? +- 主要冲突和问题 +- 主要势力和人物 + +**父辈代**(30-50年前): + +- 什么事件塑造了当代? +- 老一辈记得什么? +- 遗留的问题 + +**祖辈代**(60-100年前): + +- 传说和故事 +- 失落的知识 +- 历史创伤 + +**古代**(更久远): + +- 神话和传说 +- 文明的兴衰 +- 为当代设定埋下种子 + +## 种族与生物设计 + +### 智慧种族 + +**避免单一特征**: +❌ 所有精灵都优雅高贵 +❌ 所有矮人都贪婪暴躁 +❌ 所有兽人都野蛮好战 + +**创造深度**: +✅ 内部多样性(不同文化、价值观) +✅ 个体差异(性格各异) +✅ 历史复杂性(好的和坏的历史) + +**种族特征应该有原因**: + +- 生理特征如何帮助生存? +- 文化特征从何而来? +- 与其他种族的关系历史 + +### 魔法生物 + +**设计原则**: + +1. **生态位** + - 它在生态系统中的角色? + - 吃什么?被什么吃? + - 如何繁殖? + +2. **魔法来源** + - 为什么有魔法? + - 魔法如何帮助它生存? + - 有什么代价或限制? + +3. **与人类的关系** + - 危险还是有用? + - 能驯养吗? + - 人类如何应对? + +## 常见陷阱 + +### ❌ 过度解释 + +**问题**:花费章节解释世界设定,停止故事推进 + +**解决**: + +- 在行动中展示设定 +- 只解释角色需要知道的 +- 让读者自己拼凑一些东西 +- 信息应该服务于情节或角色 + +### ❌ 不一致的规则 + +**问题**:魔法/世界规则为了情节便利而改变 + +**解决**: + +- 提前建立所有主要规则 +- 跟踪已建立的规则 +- 例外需要早期暗示 +- 让角色在限制内创造性地解决问题 + +### ❌ 欧洲中世纪默认设置 + +**问题**:所有奇幻都是欧洲中世纪的翻版 + +**解决**: + +- 探索其他文化和时期 +- 混合多个文化元素 +- 创造独特的社会结构 +- 考虑魔法如何改变社会发展 + +### ❌ 选中之人陷阱 + +**问题**:主角因为预言/血统特殊,而非行动 + +**解决**: + +- 让主角通过选择变得特殊 +- 即使有预言,也让他们努力实现 +- 颠覆或解构陷阱 +- 专注于角色成长,而非天赋 + +## 奇幻子类型 + +### 高奇幻(史诗奇幻) + +- 完全虚构的世界 +- 善恶对抗 +- 史诗般的规模和风险 +- 魔法是世界的一部分 + +### 低奇幻 + +- 现实世界+魔法元素 +- 较小的个人风险 +- 魔法稀有且神秘 +- 更接地气的基调 + +### 城市奇幻 + +- 现代城市设定 +- 隐藏的魔法世界 +- 通常有侦探/神秘元素 +- 两个世界的碰撞 + +### 黑暗奇幻 + +- 道德灰色 +- 恐怖和暴力元素 +- 反英雄主角 +- 更严酷的后果 + +## 与 Novel-Writer 命令集成 + +### 当 `/specify` 执行时 + +- 定义核心世界元素(魔法、种族、地理) +- 识别奇幻子类型 +- 列出必须建立的主要规则 +- 计划信息揭示的节奏 + +### 在 `/plan` 期间 + +- 绘制世界元素如何影响情节 +- 计划魔法系统的展示 +- 设计不同文化的碰撞 +- 确保世界规则的一致性 + +### 在 `/write` 时 + +- 在行动中展示世界,而非倾倒 +- 让角色对世界元素做出反应 +- 使用感官细节使世界生动 +- 保持已建立规则的一致性 + +### 在 `/analyze` 期间 + +- 检查世界规则的一致性 +- 验证魔法系统的逻辑 +- 确认所有世界元素都有目的 +- 确保设定服务于故事 + +## 世界构建检查清单 + +- [ ] 魔法系统有清晰的规则和限制 +- [ ] 地理和气候影响文化和情节 +- [ ] 至少三代历史深度 +- [ ] 多个政治势力有清晰动机 +- [ ] 经济和贸易有逻辑基础 +- [ ] 种族/文化有内部多样性 +- [ ] 魔法生物有生态位和目的 +- [ ] 宗教/信仰系统有影响 +- [ ] 社会结构有明确阶级和流动性 +- [ ] 所有规则在整个故事中保持一致 + +## 信息揭示策略 + +### 冰山原则 + +**创造的 vs 展示的**: + +- 创造 100%,但只展示 10-20% +- 读者不需要知道所有东西 +- 深度创造自信和一致性 +- 作者知道的比展示的多 + +### 展示时机 + +**第一章**: + +- 基本世界观(不是详尽说明) +- 一个引人注目的魔法/奇幻元素 +- 主角在世界中的位置 + +**前 25%**: + +- 核心魔法系统规则 +- 主要种族/文化 +- 中心冲突及其世界根源 + +**中段**: + +- 深化已建立元素 +- 复杂化世界政治 +- 揭示历史影响 + +**后段**: + +- 连接所有世界线索 +- 揭示深层历史 +- 展示世界元素如何解决冲突 + +## 读者期望 + +**奇幻读者想要什么**: + +- 沉浸式、可信的世界 +- 一致且有趣的魔法系统 +- 复杂的文化和政治 +- 对经典陷阱的新鲜视角 +- 世界感觉比故事更大 + +**让奇幻读者沮丧的是什么**: + +- 不一致的世界规则 +- 方便情节的魔法 +- 单薄的欧洲中世纪抄袭 +- 缺乏深度的世界 +- 信息倾倒而非有机揭示 + +--- + +**记住**:伟大的世界构建是故事的基础,而非目的。世界应该服务于角色和情节,而角色的行动应该受到世界规则的塑造和限制。平衡深度与叙事流畅性是关键。 diff --git a/.agent/skills/genre-knowledge/mystery/SKILL.md b/.agent/skills/genre-knowledge/mystery/SKILL.md new file mode 100644 index 0000000..d017a34 --- /dev/null +++ b/.agent/skills/genre-knowledge/mystery/SKILL.md @@ -0,0 +1,374 @@ +--- +name: mystery-novel-conventions +description: "当用户提到悬疑、侦探、犯罪或悬念叙事时使用 - 提供类型规范、线索布置和推理小说的公平游戏原则" +allowed-tools: Read, Grep +--- + +# 悬疑推理小说创作规范 + +## 快速参考 + +| 元素 | 指导原则 | 位置 | +| ------------ | -------------- | ---------------- | +| **触发事件** | 案件/谜团发生 | 前 10% | +| **误导线索** | 错误的引导 | 贯穿全文,3-5 个 | +| **真实线索** | 公平游戏的证据 | 75% 之前 | +| **真相揭露** | 真相揭示 | 85-95% | +| **收尾** | 结束所有线索 | 最后 5% | + +## 核心原则 + +### 公平游戏原则 + +**黄金法则**:读者必须在侦探之前获得所有解决谜团所需的线索。 + +1. **没有隐藏信息** + - 所有关键线索都必须呈现给读者 + - 侦探不能基于读者不知道的信息破案 + - 只在结尾揭示的秘密证据违反公平游戏 + +2. **逻辑推理** + - 解决方案必须能从呈现的事实中逻辑推导 + - 巧合可以使情况复杂化,但永远不能解决 + - 直觉可以,但必须基于已展示的证据 + +3. **不使用机械降神** + - 不能突然出现新角色作为罪犯 + - 不能有之前未提及的能力或工具 + - 不能有神的干预或纯粹的运气 + +## 悬疑小说结构 + +### 第一幕:铺垫(0-25%) + +**建立常态世界**: + +- 介绍主角和他们的世界 +- 展示角色能力 +- 埋下性格怪癖的种子 + +**案件发生**: + +- 在前 10% 发生 +- 必须足够有趣/不寻常以证明调查的必要性 +- 风险应该清晰 + +**初步调查**: + +- 主角接受案件 +- 首次访谈和证据收集 +- 建立关键嫌疑人 + +### 第二幕:调查(25-75%) + +**收集线索**: + +- 呈现所有公平游戏的证据 +- 混合真实线索与误导线索 +- 每条线索都应该感觉重要 + +**误导线索**: + +- 3-5 条看似有希望的虚假线索 +- 必须足够可信以误导 +- 最终通过逻辑调查证伪 + +**复杂化升级**: + +- 新证据与旧理论矛盾 +- 嫌疑人有不在场证明或秘密 +- 风险升级(更多案件、主角面临危险) + +**中点转折**(约 50%): + +- 重新框架谜团的重大揭示 +- 主角的理论被证明错误 +- 新角度出现 + +### 第三幕:解决(75-100%) + +**黑暗之夜**(75-85%): + +- 主角似乎被难住 +- 所有理论都失败了 +- 绝望或怀疑的时刻 + +**真相揭露**(85-95%): + +- 关键洞察连接所有点 +- 主角重构真相 +- 与罪犯对峙 + +**收尾**(95-100%): + +- 解释如何/为什么 +- 所有松散的线索被收紧 +- 正义得到伸张(或有意颠覆) + +## 线索布置策略 + +### 线索类型 + +**物理证据**: + +- 物品、指纹、DNA +- 必须在需要之前埋下 +- 重要性最初可能不清楚 + +**证言证据**: + +- 证人陈述 +- 不在场证明及其矛盾 +- 谎言(有意或无意) + +**行为证据**: + +- 角色对事件的反应 +- 不寻常的行为模式 +- 通过行动揭示的动机 + +**circumstantial证据**: + +- 机会、手段、动机 +- 模式和联系 +- 时间线不一致 + +### 线索布置时机 + +**早期线索**(0-25%): + +- 建立基准事实 +- 埋下看似无辜的种子 +- 介绍所有关键嫌疑人 + +**中期线索**(25-75%): + +- 混合真实线索与误导线索 +- 使情况复杂化 +- 揭示角色动机 + +**后期线索**(75-85%): + +- 使其可解的最后一块拼图 +- 可以是一直存在的东西 +- 主角的顿悟时刻 + +## 误导线索最佳实践 + +### 有效的误导线索 + +**特征**: + +- 足够可信以显得真实 +- 有一些证据支持 +- 最终通过逻辑被证伪 +- 揭示为虚假时不显得廉价 + +**示例**: + +- 有强烈动机但坚实不在场证明的嫌疑人 +- 被栽赃的有罪证据 +- 巧合出现在犯罪现场 +- 看起来有罪但实际无辜的秘密活动 + +### 常见错误 + +❌ **太明显**:读者立即看穿 +❌ **太勉强**:感觉强迫和人为 +❌ **从不解释**:悬而未决没有解决 +❌ **太多**:读者失去追踪并感到沮丧 + +## 嫌疑人管理 + +### 经典设置 + +**至少 3 个嫌疑人**: + +- 每个都需要动机、手段和机会 +- 每个在某个时刻都应该显得有罪 +- 至少一个应该是同情的 + +**罪犯**: + +- 应该在介绍的角色中(公平游戏) +- 必须有最终揭示的逻辑动机 +- 他们有罪的线索必须从早期就存在 + +**误导嫌疑人**: + +- 最明显的选择 +- 强烈的动机和间接证据 +- 最终通过调查被排除 + +**同情嫌疑人**: + +- 读者希望不是有罪的 +- 有值得保护的秘密 +- 通常帮助解决真正的谜团 + +### 角色秘密 + +**每个嫌疑人都应该有秘密**: + +- 不是所有秘密都与案件相关 +- 秘密创造误导 +- 揭示秘密推进调查 +- 有些秘密比案件更具破坏性 + +## 常见陷阱 + +### ❌ 不可知的解决方案 + +**问题**:罪犯或方法依赖读者没有的信息 + +**解决**:在 75% 标记之前埋下所有必要线索;读者应该能够解决 + +### ❌ 无能的侦探 + +**问题**:主角错过明显线索或行为不合逻辑 + +**解决**:让侦探有能力但人性化;他们可以犯错,但不是愚蠢 + +### ❌ 太多巧合 + +**问题**:情节通过方便的运气而非调查推进 + +**解决**:巧合可以使情况复杂化,永远不能解决;侦探必须努力寻找答案 + +### ❌ 无聊的中段 + +**问题**:调查变成重复的访谈接访谈 + +**解决**:变化调查方法;添加动作、危险、个人风险 + +### ❌ 仓促的解释 + +**问题**:复杂的解决方案在最后一章的对话中倾倒 + +**解决**:分散揭示;让读者拼凑;保持解释清晰但不冗长 + +## 子类型变化 + +### 温馨推理 + +- 业余侦探 +- 有限的暴力描写 +- 小社区背景 +- 角色驱动 +- 通常幽默的基调 + +### 硬派侦探 + +- 专业调查员 +- 粗糙、现实的暴力 +- 道德复杂的世界 +- 愤世嫉俗的基调 +- 动作导向 + +### 警察程序 + +- 专注于调查过程 +- 多个侦探/团队 +- 现实的程序 +- 技术细节重要 +- 官僚主义作为障碍 + +### 密室推理 + +- 不可能的犯罪场景 +- 有限的嫌疑人(谁有机会) +- 巧妙的方法是关键 +- 解决方案必须合乎逻辑 + +## 与 Novel-Writer 命令集成 + +### 当 `/specify` 执行时 + +- 清晰定义中心谜团 +- 列出所有主要嫌疑人及其动机 +- 识别关键线索及其出现位置 +- 决定公平游戏规则 + +### 在 `/plan` 期间 + +- 绘制线索布置时间线 +- 设计误导线索模式 +- 计划调查序列 +- 结构揭示和反转 + +### 在 `/write` 时 + +- 确保线索可见但不明显 +- 平衡调查与角色发展 +- 保持节奏(动作、揭示、复杂化) +- 跟踪读者知道什么vs侦探知道什么 + +### 在 `/analyze` 期间 + +- 验证公平游戏 - 读者能解决吗? +- 检查所有线索是否已埋下 +- 确保没有机械降神 +- 确认令人满意的解决 + +## 悬疑写作检查清单 + +- [ ] 中心谜团引人入胜且清晰 +- [ ] 3-5 个有动机的可行嫌疑人 +- [ ] 揭示前呈现所有关键线索 +- [ ] 误导线索可信且最终得到解释 +- [ ] 侦探有能力且合乎逻辑 +- [ ] 解决方案可从给定信息推导 +- [ ] 没有巧合解决谜团 +- [ ] 时间线一致且可追踪 +- [ ] 所有松散的线索都被收紧 +- [ ] 揭示令人满意,而非令人失望 + +## 线索可见性框架 + +### 三个层次 + +**层次 1 - 明显**(25% 的线索): + +- 引入时明确重要 +- 主角和读者一起注意到 +- 建立基准事实 + +**层次 2 - 微妙**(50% 的线索): + +- 提及但不强调 +- 重要性后来变得清晰 +- 奖励细心的读者 + +**层次 3 - 藏在显眼处**(25% 的线索): + +- 引入时看似无关紧要 +- 只有在回顾时才有意义 +- "啊哈!"时刻 + +### 示例 + +**层次 1**:"窗户从里面解锁" +**层次 2**:角色在闲聊中提到自己是左撇子 +**层次 3**:房间描述包括烟灰缸中的特定品牌香烟 + +## 读者期望 + +**悬疑读者想要什么**: + +- 与侦探一起解决的公平机会 +- 事后有意义的聪明转折 +- 有能力但会犯错的主角 +- 令人满意的"啊哈!"时刻 +- 正义(或有目的的有意颠覆) + +**让悬疑读者沮丧的是什么**: + +- 只在结尾揭示的隐藏信息 +- 通过未展示的推理解决的主角 +- 勉强的巧合 +- 没有误导的明显罪犯 +- 松散的线索悬而未决 + +--- + +**记住**:一个伟大的谜团让读者因解决它而感到聪明,或因没有看到它而印象深刻 - 但总是满意线索一直都在那里。 diff --git a/.agent/skills/genre-knowledge/romance/SKILL.md b/.agent/skills/genre-knowledge/romance/SKILL.md new file mode 100644 index 0000000..dea9c04 --- /dev/null +++ b/.agent/skills/genre-knowledge/romance/SKILL.md @@ -0,0 +1,244 @@ +--- +name: romance-novel-conventions +description: "当用户提到言情、爱情故事或关系导向叙事时使用 - 提供类型规范、节奏指南和言情小说的情感节点" +allowed-tools: Read, Grep +--- + +# 言情小说创作规范 + +## 快速参考 + +| 元素 | 指导原则 | 示例 | +| ------------ | ------------------------------- | ------------------------ | +| **初遇** | 故事的 0-10% | 意外的首次相遇,产生火花 | +| **初吻** | 50-60%(慢热)或 20-30%(快热) | 需要充分的情感铺垫 | +| **黑暗时刻** | 75-85% | 关系看似不可能继续 | +| **大结局** | 85-100% | 表白、承诺、HEA/HFN | + +## 核心要素 + +### 必备组成部分 + +1. **情感连接**:主角之间必须有化学反应 + - 通过行动而非单纯描述来展现吸引力 + - 通过亲近和距离建立张力 + - 创造可信的相互吸引的理由 + +2. **内在冲突**:恐惧、过往创伤、自我怀疑 + - 每个角色都应该有情感包袱 + - 内在冲突必须与外在障碍同样强烈 + - 成长来自面对这些内在问题 + +3. **外在障碍**:家庭、事业、误会 + - 障碍应该是合理的,而非人为的 + - "只要好好沟通"不应该能解决所有问题 + - 风险应该逐步升级 + +4. **令人满意的结局**:HEA(永远幸福)或 HFN(暂时幸福) + - 两个角色都必须成长才配得上结局 + - 结局应该解决内在和外在冲突 + - 读者应该感到满足,而非被欺骗 + +### 可选元素 + +- 三角恋(谨慎使用 - 可能让读者沮丧) +- 被迫亲密设定(被困在一起的场景) +- 欢喜冤家(需要精心处理) +- 第二次机会言情(重燃旧情) + +## 节奏指南 + +| 故事阶段 | 百分比 | 关键事件 | 情感焦点 | +| ------------ | ------- | ------------------------ | ---------- | +| **初遇** | 0-10% | 初次相遇,产生兴趣的火花 | 好奇、吸引 | +| **张力建立** | 10-60% | 吸引力增长,障碍出现 | 渴望、挫折 | +| **冲突升级** | 60-75% | 重大误会或真相揭露 | 怀疑、痛苦 | +| **黑暗时刻** | 75-85% | 关系看似不可能 | 绝望、失落 | +| **大结局** | 85-100% | 盛大表白、承诺 | 喜悦、圆满 | + +## 情感节奏点 + +### 初吻时机 + +**慢热言情**(约 50-60%): + +- 初吻前有多次差点亲上的时刻 +- 大量的情感铺垫 +- 亲吻发生在建立重要信任之后 +- 读者的期待值很高 + +**快热言情**(约 20-30%): + +- 立即的化学反应导致快速的身体接触 +- 之后重点转移到情感亲密 +- 仍必须展现合理的吸引力建立 +- 避免"一见钟情" - 展示他们为何吸引 + +**黄金法则**:在没有足够铺垫的情况下,绝不要仓促。没有情感基础的身体亲密会显得空洞。 + +### 亲密关系进展 + +1. **情感亲密先于身体亲密** + - 分享脆弱性 + - 揭露过去的伤痛 + - 信任必须赢得 + +2. **逐步展现脆弱** + - 从小的袒露开始 + - 建立到更深层的情感分享 + - 身体亲密跟随情感信任 + +3. **尊重角色界限** + - 角色可以说不 + - 同意是必不可少的 + - 尊重符合角色性格的节奏 + +## 常见陷阱 + +### ❌ 一见钟情 + +**问题**:角色在没有发展的情况下爱得太快 + +**为什么不好**:读者不相信这种连接;感觉勉强和不真实 + +**解决方法**: + +- 在表白之前展示 3-5 次有意义的互动 +- 通过行动而非想法建立吸引力 +- 给出他们相互吸引的具体原因 +- 允许时间让信任发展 + +### ❌ 误会是唯一冲突 + +**问题**:"只要好好谈谈,一切都会好起来" + +**为什么不好**:让读者沮丧;感觉像人为的戏剧 + +**解决方法**: + +- 添加合理的外部障碍(工作、家庭、地点) +- 创造必须发生的内在成长(恐惧、创伤、身份认同) +- 让沟通因真实原因而困难(权力失衡、过去的背叛) +- 确保冲突需要角色成长才能解决 + +### ❌ 被动的主角 + +**问题**:等待被拯救或被选择;没有主动权 + +**为什么不好**:削弱角色,让言情感觉不平等 + +**解决方法**: + +- 给他们关系之外的目标 +- 展示他们主动追求 +- 让他们为爱情积极做出牺牲 +- 确保两个角色在关系中拥有平等的主动权 + +### ❌ 陈词滥调没有创新 + +**问题**:读者已经看过一千次的可预测情节 + +**为什么不好**:无聊;读者能预测每一个转折 + +**解决方法**: + +- 添加独特的角色背景或情境 +- 在熟悉的框架内扭转期望 +- 以意想不到的方式结合套路 +- 让角色性格驱动情节,而非反过来 + +## 子类型考虑 + +### 现代言情 + +- 现代关系动态 +- 现实的障碍(事业、距离、时机) +- 短信/科技发挥作用 +- 关注兼容性和沟通 + +### 古代言情 + +- 符合时代的限制(阶级、礼仪) +- 社会规则创造自然障碍 +- 需要研究以确保真实性 +- 平衡历史准确性与现代敏感性 + +### 超自然言情 + +- 超自然元素增加风险 +- 不朽创造时间线问题 +- 必须解决权力动态 +- 危险可以迫使亲密 + +### 悬疑言情 + +- 外部危险将他们联系在一起 +- 秘密使信任复杂化 +- 悬疑情节必须同样强大 +- 结局需要解决两个谜团 + +## 与 Novel-Writer 命令集成 + +### 当用户执行 `/specify` 时 + +- 提醒在故事结构中包含关系弧 +- 建议定义:初遇、主要障碍、黑暗时刻、大结局 +- 识别言情子类型以适应相应惯例 + +### 在 `/plan` 期间 + +- 将情感节奏映射到章节结构 +- 计划被迫亲密或分离的时刻 +- 设计内外冲突升级 +- 确保身体/情感亲密的适当节奏 + +### 在 `/write` 时 + +- 为浪漫张力应用对话技巧 +- 通过行动而非想法展现化学反应 +- 通过潜台词建立性张力 +- 确保两个角色都有主动权 + +### 当 `/analyze` 运行时 + +- 根据言情惯例检查节奏 +- 验证两个角色都有完整的弧线 +- 确保障碍感觉合理,而非人为 +- 确认令人满意的 HEA/HFN 结局 + +## 实用检查清单 + +写言情时,确保你有: + +- [ ] 早期建立强烈的化学反应(10% 前) +- [ ] 两位主角都有清晰的内在冲突 +- [ ] 不能通过一次对话解决的外部障碍 +- [ ] 整个故事中跟踪的情感亲密发展 +- [ ] 适合子类型的身体亲密节奏 +- [ ] 感觉毁灭性且真实的黑暗时刻 +- [ ] 解决两个角色成长的结局 +- [ ] 满足读者期望的 HEA 或 HFN + +## 常见读者期望 + +**言情读者想要什么**: + +- 情感满足胜过惊喜 +- 两个角色都赢得他们的幸福 +- 通过成长克服可信的障碍 +- 页面上闪耀的化学反应 +- 他们想要给自己的关系 +- 所有关系线索的结尾 + +**让言情读者沮丧的是什么**: + +- 可以轻易解决的人为冲突 +- 一个角色做所有情感工作 +- 没有适当解决的仓促结局 +- 未解决的不平等权力动态 +- 没有后果的出轨或背叛 +- 为了情节便利而牺牲角色成长 + +--- + +**记住**:言情是关于两个人在一起变得比分开时更好的旅程。障碍应该迫使成长,化学反应应该感觉不可避免,结局应该感觉是赢得的。 diff --git a/.agent/skills/novelweave-workflow/SKILL.md b/.agent/skills/novelweave-workflow/SKILL.md new file mode 100644 index 0000000..fc2c332 --- /dev/null +++ b/.agent/skills/novelweave-workflow/SKILL.md @@ -0,0 +1,185 @@ +--- +name: novelweave-workflow +description: 使用 NovelWeave 进行小说创作的完整工作流程,包括命令使用、最佳实践和高效创作技巧。适用于规划小说项目、组织创作过程或学习 NovelWeave 功能。 +version: 1.0.0 +keywords: [NovelWeave, 工作流, 小说命令, 创作流程, 最佳实践] +when_to_use: 开始新小说项目、优化创作流程、学习 NovelWeave 功能或需要创作指导时使用 +allowed_tool_groups: [read] +--- + +# NovelWeave 小说创作工作流 + +## NovelWeave 核心理念 + +NovelWeave 是一个 AI 驱动的小说创作助手,设计用于支持从构思到完稿的整个创作过程。 + +### 三大支柱 + +1. **结构化创作** - 通过专用命令引导创作流程 +2. **知识管理** - 使用 Knowledge Base 追踪世界观和角色 +3. **AI 协作** - 与 AI 共同创作,而非让 AI 独立创作 + +## 完整创作工作流 + +### 阶段 1:构思和规划 + +#### 1.1 定义创作核心(Constitution) + +使用 `/constitution` 命令建立小说的核心价值观和主题。 + +**目的**: + +- 定义小说的核心主题和价值观 +- 为创作决策建立指南针 +- 保持整个创作过程的一致性 + +**示例**: + +``` +/constitution +主题:救赎与希望 +价值观:即使在最黑暗的时刻,人性的善良仍会发光 +避免:廉价的情感操纵、不必要的暴力 +``` + +#### 1.2 制定创作计划 + +使用 `/plan` 命令构建小说大纲和结构。 + +**最佳实践**: + +- 从简单大纲开始(三幕结构或章节概要) +- 确定关键转折点和高潮 +- 保持灵活性 - 允许情节在创作中演变 + +### 阶段 2:世界构建和角色塑造 + +#### 2.1 建立角色档案 + +使用 Agent Rules 和 Knowledge Base 创建详细的角色档案。 + +**关键要素**: + +- 外貌、性格、动机 +- 背景故事和创伤 +- 角色弧线和成长轨迹 +- 语言风格和习惯用语 + +#### 2.2 构建世界设定 + +为奇幻、科幻或复杂背景建立世界观。 + +**记录内容**: + +- 地理和地点 +- 历史和文化 +- 魔法系统或科技规则 +- 社会结构和政治 + +### 阶段 3:场景创作 + +#### 3.1 使用 `/write` 命令 + +这是核心创作命令,用于生成场景内容。 + +**有效的 `/write` 请求**: + +``` +/write +场景:艾米在废弃工厂与追踪者对峙 +情感:紧张、恐惧但决心坚定 +重点:展示艾米的机智和勇气 +长度:800-1000 字 +``` + +**避免**: + +- 过于宽泛的请求("写第一章") +- 缺乏情感方向 +- 没有明确的场景目标 + +#### 3.2 追踪场景连续性 + +使用 `/track` 系统维护情节、角色和时间线的一致性。 + +**功能**: + +- 追踪角色状态和情感弧线 +- 记录重要事件和时间戳 +- 识别潜在的情节漏洞 + +### 阶段 4:审稿和修订 + +#### 4.1 质量保证 + +使用一致性检查工具审查稿件。 + +**检查领域**: + +- 角色一致性(外貌、性格、声音) +- 情节连续性 +- 时间线准确性 +- 主题一致性 + +#### 4.2 风格润色 + +审查对话、节奏和描写质量。 + +**常见改进点**: + +- 对话真实性 +- 展示 vs. 告知平衡 +- 节奏和张力 +- 感官细节 + +## NovelWeave 最佳实践 + +### 1. 增量创作 + +- 一次专注于一个场景 +- 先完成粗稿,再精炼 +- 允许灵感自然涌现 + +### 2. 充分利用 Knowledge Base + +- 记录所有重要细节 +- 定期审查以确保一致性 +- 在创作时参考现有材料 + +### 3. 与 AI 有效协作 + +- 提供明确的方向和上下文 +- 审查并编辑 AI 生成的内容 +- 保持你独特的创作声音 + +### 4. 保持灵活性 + +- 允许故事自然演变 +- 不要过度规划 +- 相信创作过程 + +## 常见陷阱避免 + +❌ **过度依赖 AI** - 你是作者,AI 是助手 +❌ **跳过规划** - 一些结构能防止后期大返工 +❌ **忽视一致性** - 小错误会累积破坏可信度 +❌ **一次写太多** - 专注质量而非数量 + +## 快速参考:关键命令 + +- `/constitution` - 定义核心价值观和主题 +- `/plan` - 创建和管理小说大纲 +- `/write` - 生成场景内容 +- `/track` - 追踪情节和角色一致性 +- `/clarify` - 解决情节问题和填补漏洞 +- `/analyze` - 深入分析文本和结构 + +## 与其他 Skills 配合 + +- **类型知识** Skills(奇幻、言情、悬疑)提供类型特定指导 +- **写作技巧** Skills(对话、场景结构)提升技艺水平 +- **质量保证** Skills(一致性检查)确保专业品质 + +--- + +**记住**:NovelWeave 旨在增强你的创造力,而非替代它。保持你的独特声音,让 AI 帮助你实现愿景。 diff --git a/.agent/skills/quality-assurance/consistency-checker/SKILL.md b/.agent/skills/quality-assurance/consistency-checker/SKILL.md new file mode 100644 index 0000000..deda3c7 --- /dev/null +++ b/.agent/skills/quality-assurance/consistency-checker/SKILL.md @@ -0,0 +1,367 @@ +--- +name: story-consistency-monitor +description: "在章节写作过程中自动检查角色行为、世界规则和时间线一致性 - 在潜在矛盾成为重大问题前发出警报" +allowed-tools: Read, Grep +--- + +# 故事一致性监控 + +## 自动检查系统 + +### 本技能监控什么 + +#### 角色一致性 + +- **物理特征**:眼睛颜色、身高、年龄、疤痕 +- **性格**:行动符合已建立的角色 +- **知识**:角色只知道他们应该知道的 +- **成长**:变化与角色弧线一致 + +#### 世界规则 + +- **魔法/科技系统**:力量的使用一致 +- **地理**:距离和地点保持稳定 +- **社会规则**:文化和习俗不矛盾 +- **物理法则**:已建立的规则不随机打破 + +#### 时间线逻辑 + +- **事件顺序**:A 在逻辑上发生在 B 之前 +- **时间流逝**:角色适当地老化 +- **同时事件**:多 POV 时间线对齐 +- **历史一致性**:过去的引用保持一致 + +### 如何工作 + +**被动监控**:当你写作或讨论故事时,我会自动交叉参考: + +1. `characters/` 目录中的角色档案 +2. `worldbuilding/` 目录中的世界构建文档 +3. `spec/tracking/timeline.json` 中的时间线数据 +4. 之前章节的内容 + +**不需要你采取任何行动** - 监控在后台进行。 + +## 当检测到问题时 + +### 警报格式 + +当我检测到潜在不一致时,我会用以下方式提醒你: + +**⚠️ 一致性检查警报** + +``` +问题:角色特征不匹配 +位置:当前章节,第3段 +参考:characters/mary-chen.md,第15行 + +当前文本:"玛丽的绿色眼睛眯起..." +已建立特征:"眼睛颜色:蓝色"(在第3章中设定) + +可能的解决方案: +1. 将当前文本改为"蓝色眼睛" +2. 如果你要修改设定,更新角色档案 +3. 这是一个有相似名字的不同角色? + +你想让我自动修复这个,还是你更愿意自己处理? +``` + +### 严重程度级别 + +| 级别 | 图标 | 行动 | 示例 | +| -------- | ---- | -------------- | ------------------------------------ | +| **关键** | 🔴 | 立即停止并修复 | 角色突然知道他们不应该知道的秘密信息 | +| **警告** | ⚠️ | 尽快修复 | 角色的惯常言语模式改变了 | +| **注意** | 📝 | 考虑检查 | 时间线感觉压缩 | + +## 与 Novel-Writer 命令集成 + +### 在 `/write` 期间 + +- 在生成内容时进行实时一致性检查 +- 对关键问题的即时警报 +- 自动参考规格文档 + +### 在 `/analyze` 期间 + +- 全面的一致性报告 +- 所有累积的警告和注意事项 +- 建议的修复按严重性排序 + +### 在 `/track` 期间 + +- 使用经过验证的信息更新追踪数据 +- 标记不一致以供手动审查 +- 维护一致性历史 + +## 配置 + +### 严格程度级别 + +你可以调整一致性检查的严格程度: + +**严格模式**(非奇幻的默认): + +- 标记所有矛盾 +- 执行真实世界物理 +- 时间线必须完全合乎逻辑 + +**灵活模式**(推荐用于奇幻/科幻): + +- 允许"酷炫规则"例外 +- 魔法/科技可以弯曲现实 +- 允许艺术许可,但会通知 + +**最小模式**: + +- 只标记关键矛盾 +- 专注于角色和主要情节点 +- 让小的不一致通过 + +### 禁用特定检查 + +如果某些不一致是有意的: + +``` +"请为梦境序列禁用时间线检查 - +它们有意是非线性的。" +``` + +## 常见误报 + +有时我会标记实际上正确的东西: + +### 有意的矛盾 + +**示例**:角色谎报眼睛颜色 +**修复**:在角色档案中添加评论:"// 眼睛实际上是蓝色的,告诉人们是绿色的" + +### 不可靠的叙述者 + +**示例**:第一人称叙述者记错事件 +**修复**:在宪法中注明:"不可靠的叙述者 - 记忆不一致是有意的" + +### 时间跳跃 + +**示例**:角色的年龄突然增加 +**修复**:在章节中明确说明时间跳跃:"三年后..." + +## 最佳实践 + +### 保持参考文档更新 + +一致性检查器只能和你的文档一样好: + +- 特征变化时更新角色档案 +- 清楚地记录世界规则 +- 使用 `/timeline` 命令维护时间线文件 + +### 及时处理警报 + +不要让一致性问题累积: + +- 立即修复关键警报 +- 在写作会话结束时审查警告 +- 在修订阶段批处理注意事项 + +### 与 `/track` 一起使用 + +一致性检查 + 追踪系统 = 强大组合: + +- `/track --check` 运行深度一致性验证 +- `/track --fix` 可以自动修复简单问题 +- 定期使用两者(每 5-10 章) + +## 检查类别详解 + +### 角色一致性检查 + +**物理描述**: + +``` +✓ 检查:身高、体重、年龄、发色、眼色 +✓ 检查:疤痕、纹身、独特标记 +✓ 检查:服装风格、配饰 +``` + +**行为模式**: + +``` +✓ 检查:言语模式是否一致 +✓ 检查:反应是否符合性格 +✓ 检查:决策是否符合价值观 +✓ 检查:技能/能力是否一致 +``` + +**知识状态**: + +``` +✓ 检查:角色知道什么时候知道的 +✓ 检查:他们不知道不应该知道的秘密 +✓ 检查:记忆与已建立事实一致 +``` + +### 世界规则检查 + +**魔法/科技系统**: + +``` +✓ 检查:力量在已建立限制内使用 +✓ 检查:代价/成本一致应用 +✓ 检查:规则不为了情节便利而改变 +✓ 检查:例外有前期暗示 +``` + +**地理和距离**: + +``` +✓ 检查:地点在地图上保持一致 +✓ 检查:旅行时间合理 +✓ 检查:气候与地理匹配 +✓ 检查:地标不移动 +``` + +**社会和文化**: + +``` +✓ 检查:文化规范一致 +✓ 检查:语言和方言保持稳定 +✓ 检查:社会结构不随机改变 +✓ 检查:宗教/信仰保持一致 +``` + +### 时间线检查 + +**事件序列**: + +``` +✓ 检查:原因发生在结果之前 +✓ 检查:角色不在他们不能在的地方 +✓ 检查:事件在合理的时间范围内发生 +``` + +**时间流逝**: + +``` +✓ 检查:角色适当地老化 +✓ 检查:季节按顺序改变 +✓ 检查:怀孕/康复需要适当的时间 +✓ 检查:技能习得需要练习时间 +``` + +**多POV同步**: + +``` +✓ 检查:同时事件从不同POV匹配 +✓ 检查:时间跳跃在POV之间对齐 +✓ 检查:没有POV知道其他POV的未来 +``` + +## 自动修复功能 + +对于某些简单问题,我可以提供自动修复: + +### 自动修复类型 + +**拼写变化**: + +``` +检测:角色名字拼写不一致 +建议:标准化为最常见的拼写 +行动:全局查找并替换(经你批准) +``` + +**数字不一致**: + +``` +检测:角色年龄在章节间不匹配 +建议:基于时间线计算正确年龄 +行动:更新到正确数字 +``` + +**时间线冲突**: + +``` +检测:事件日期与已建立时间线冲突 +建议:调整日期以适应已知序列 +行动:更新时间线参考 +``` + +## 报告和追踪 + +### 一致性报告 + +定期(或根据要求),我会生成: + +```markdown +## 一致性报告 - [日期] + +### 章节范围:章节 1-15 + +### 检测到的问题 + +#### 关键(必须修复) + +1. 第12章:角色知道只在第14章揭示的信息 + - 修复:重写第12章场景或移动第14章揭示更早 + +#### 警告(应该修复) + +1. 第8章:角色的眼睛颜色从蓝色变为绿色 + - 修复建议:将第8章更新为蓝色或更新角色档案 +2. 第10章:从首都到边境的旅行只用了1天(之前建立为3天) + - 修复建议:添加时间跳跃或调整天数 + +#### 注意事项(考虑检查) + +1. 第5章:角色反应似乎不符合特征 + - 审查:这是有意的成长还是不一致? + +### 统计 + +- 总检查:456 +- 问题发现:8 +- 自动修复:3 +- 需要审查:5 + +### 一致性得分:94% +``` + +### 追踪历史 + +我维护发现和修复的一致性问题的历史: + +- 有助于识别模式 +- 防止重复错误 +- 显示随时间的改进 +- 对修订有用 + +## 与其他技能协作 + +### 配合 Writing Techniques Skills + +**对话一致性**: + +- 检查角色声音是否保持 +- 验证言语模式 +- 标记不符合特征的对话 + +**场景一致性**: + +- 验证设定细节 +- 检查物理可能性 +- 确认时间流逝 + +### 配合 Genre Knowledge Skills + +**类型惯例一致性**: + +- 确保类型规则应用一致 +- 检查陷阱是否一致避免 +- 验证节奏模式 + +--- + +**记住**:一致性不是关于完美 - 它是关于读者的信任。当世界规则可靠时,读者沉浸其中。当规则似乎随意改变时,他们被拉出体验。我在这里帮助维持这种信任。 + +**你总是有最后的决定权** - 如果不一致是艺术选择,告诉我,我会停止标记它。 diff --git a/.agent/skills/quality-assurance/forgotten-elements/SKILL.md b/.agent/skills/quality-assurance/forgotten-elements/SKILL.md new file mode 100644 index 0000000..93d7b6b --- /dev/null +++ b/.agent/skills/quality-assurance/forgotten-elements/SKILL.md @@ -0,0 +1,148 @@ +--- +name: forgotten-elements-reminder +description: "当重要的故事元素(角色、情节线、伏笔)10章以上未出现时自动提醒 - 防止长篇小说中的'角色消失综合症'和遗漏的情节线" +allowed-tools: Read, Grep +--- + +# 遗忘元素提醒器 + +## 核心功能 + +**防止长篇小说常见问题**: + +- 角色突然消失("配角A去哪了?") +- 情节线被遗忘("那个伏笔后来呢?") +- 伏笔没回收("前文说的宝藏呢?") + +**解决方案**:后台监控,主动提醒已经很久未出现的元素。 + +--- + +## 监控内容 + +### 1. 角色出场频率 + +``` +监控:character-state.json中的所有角色 +阈值:10章未出现 + +提醒示例: +⚠️ 角色提醒: +"配角李明"已经10章未出现(上次:第5章) +- 是否应该安排他再次出场? +- 还是这个角色的故事已结束? +``` + +### 2. 情节线进度 + +``` +监控:plot-tracker.json中的活跃情节线 +阈值:12章无进展 + +提醒示例: +⚠️ 情节线提醒: +"寻找父亲仇人"情节线已12章无进展 +- 上次推进:第8章 +- 当前状态:pending +- 建议:安排相关情节或标记为暂停 +``` + +### 3. 伏笔回收 + +``` +监控:在前文埋下的重要伏笔 +阈值:20章未回收 + +提醒示例: +⚠️ 伏笔提醒: +第3章提到"神秘盒子",至今未揭秘(已过23章) +- 读者可能已经忘记 +- 建议:尽快回收或在对话中提及 +``` + +--- + +## 提醒时机 + +### 写作前提醒 + +``` +执行 `/write` 时,如果检测到遗忘元素: + +📋 写作前检查... +⚠️ 发现3个被遗忘的元素: + +1. 角色"张婶"已15章未出现 +2. 情节线"寻宝"已13章无进展 +3. 伏笔"神秘信件"已20章未揭秘 + +💡 本章是否考虑处理? +``` + +### 分析时汇总 + +``` +执行 `/analyze` 时,生成完整报告: + +## 遗忘元素报告 + +### 失踪角色(3个) +1. 张婶(15章未出场) +2. 李老板(11章未出场) +3. 王医生(10章未出场) + +### 停滞情节线(2个) +1. 寻宝线(13章无进展) +2. 复仇线(12章无进展) + +### 未回收伏笔(1个) +1. 神秘信件(20章未揭秘) + +建议: +- 优先处理失踪角色(读者可能已忘记) +- 推进停滞情节线或标记为"暂停" +- 伏笔及时回收(否则成烂尾) +``` + +--- + +## 配置选项 + +### 调整阈值 + +``` +"角色未出场提醒阈值改为15章" +"情节线停滞提醒阈值改为20章" +``` + +### 排除特定元素 + +``` +"张婶角色已完结,不再提醒" +"寻宝线有意暂停,30章后才推进,不提醒" +``` + +--- + +## 最佳实践 + +1. **定期查看**:每10章运行一次 `/analyze` 查看报告 +2. **及时处理**:不要让太多元素堆积 +3. **主动标记**:已完结的角色标记为"retired" + +--- + +## 总结 + +forgotten-elements-reminder = 你的**记忆助手** + +✓ 自动监控角色/情节/伏笔 +✓ 超过阈值主动提醒 +✓ 防止长篇小说烂尾 + +**长篇小说必备!** 📝 + +--- + +**本Skill版本**: v1.0 +**最后更新**: 2025-10-18 diff --git a/.agent/skills/quality-assurance/getting-started/SKILL.md b/.agent/skills/quality-assurance/getting-started/SKILL.md new file mode 100644 index 0000000..e6786f7 --- /dev/null +++ b/.agent/skills/quality-assurance/getting-started/SKILL.md @@ -0,0 +1,225 @@ +--- +name: getting-started-guide +description: "当用户开始新小说项目时激活 - 通过温和的提示和解释引导他们完成七步方法论(constitution → specify → clarify → plan → tasks → write → analyze)" +allowed-tools: Read +--- + +# 新用户入门引导 + +## 激活条件 + +当用户说出以下内容时自动激活: + +- "我想写小说" +- "我要开始一个新项目" +- "怎么使用novel-writer" +- "从哪里开始" + +--- + +## 七步方法论引导 + +### 欢迎消息 + +``` +👋 欢迎使用Novel Writer Skills! + +我注意到你要开始一个新的小说项目。 +Novel Writer使用系统化的七步方法论,帮助你从想法到完稿。 + +让我带你快速了解流程: + +第1步:/constitution - 创建创作宪法 +第2步:/specify - 定义故事规格 +第3步:/clarify - 澄清模糊点(5个问题) +第4步:/plan - 制定创作计划 +第5步:/tasks - 分解任务清单 +第6步:/write - 执行章节写作 +第7步:/analyze - 质量验证分析 + +想要详细了解每一步吗?还是直接开始第1步? +``` + +### 步骤详解(根据需要提供) + +``` +【第1步:/constitution】 +创建你的创作宪法 - 这是最高原则。 +- 核心价值观(你想传达什么?) +- 质量标准(什么算好的章节?) +- 禁忌红线(不能写什么?) + +为什么重要:后续所有创作都要符合宪法。 + +【第2步:/specify】 +定义故事规格 - 明确你要写什么。 +- 故事概念、类型、主题 +- 目标读者、字数、结局 +- 必须包含的元素(P0) + +为什么重要:规格是技术需求,避免写到一半发现偏题。 + +【第3步:/clarify】 +AI提出5个关键问题,帮你澄清模糊点。 +- 针对specification中的不明确之处 +- 逼迫你想清楚细节 + +为什么重要:模糊的规格=模糊的故事。 + +【第4步:/plan】 +制定创作计划 - 技术方案。 +- 章节架构、节奏设计 +- 关键情节点时间线 +- 技术难点的解决方案 + +为什么重要:计划详细,执行才顺畅。 + +【第5步:/tasks】 +分解任务清单 - 可执行的步骤。 +- 每个章节一个任务 +- 标记状态(pending/in_progress/completed) +- 追踪进度 + +为什么重要:大目标拆解成小任务,不overwhelm。 + +【第6步:/write】 +执行写作 - 基于任务清单。 +- 自动加载所有context +- 遵循宪法、规格、计划 +- 质量自检后保存 + +为什么重要:这是七步方法论的优势集中体现。 + +【第7步:/analyze】 +质量验证 - 确保一致性。 +- 框架检查(结构、节奏) +- 内容检查(一致性、质量) +- 发现问题,建议修复 + +为什么重要:定期分析,及时调整,避免大返工。 +``` + +--- + +## 常见问题快速解答 + +### Q: 必须按顺序执行吗? + +**A**: 推荐按顺序,但不强制。 + +``` +推荐流程:1→2→3→4→5→6→7(循环6-7) + +允许跳步: +- 有经验的作者可以跳过/clarify +- 短篇可以简化/plan + +不推荐: +- 直接跳到/write(缺少规格和计划,容易写乱) +``` + +### Q: 七步走完要多久? + +**A**: 取决于故事复杂度。 + +``` +简单故事(5万字短篇): +- 步骤1-5:2-3小时 +- 步骤6-7:执行写作,持续进行 + +复杂故事(50万字长篇): +- 步骤1-5:1-2天 +- 步骤6-7:数月 + +前期投入换来后期顺畅。 +``` + +### Q: 可以中途修改吗? + +**A**: 当然! + +``` +发现规格不对: +→ 修改specification.md +→ 重新运行/plan调整计划 + +计划需要调整: +→ 修改creative-plan.md +→ 更新tasks.md + +这是迭代过程,不是一次定死。 +``` + +--- + +## 第一次使用建议 + +### 从简单项目开始 + +``` +✓ 推荐: +- 5-10万字的中短篇 +- 单一类型(言情/悬疑/历史) +- 简单情节(1-2条主线) + +⚠️ 不推荐初次就写: +- 50万字+的长篇 +- 多类型融合(如言情+悬疑+奇幻+历史) +- 超复杂世界观 + +先熟悉流程,再挑战复杂项目。 +``` + +### 执行第一个命令 + +``` +准备好了?让我们开始第1步: + +执行命令:/constitution + +我会引导你创建创作宪法。 +准备回答以下问题: +1. 这个故事你想传达什么核心理念? +2. 你对质量的标准是什么? +3. 有哪些内容是绝对不能写的? + +准备好了就输入:/constitution +``` + +--- + +## 与workflow-guide的配合 + +``` +getting-started: +- 初次使用时激活 +- 提供完整流程概览 +- 回答入门问题 + +workflow-guide: +- 整个创作过程中持续激活 +- 偏离流程时温和提醒 +- 提供最佳实践建议 + +两者互补: +getting-started = 入门教程 +workflow-guide = 持续顾问 +``` + +--- + +## 总结 + +getting-started-guide = 你的**入门向导** + +✓ 自动识别新用户 +✓ 引导七步方法论 +✓ 回答常见问题 +✓ 给出第一步建议 + +**让第一次使用不再迷茫!** 🚀 + +--- + +**本Skill版本**: v1.0 +**最后更新**: 2025-10-18 diff --git a/.agent/skills/quality-assurance/pre-write-checklist/SKILL.md b/.agent/skills/quality-assurance/pre-write-checklist/SKILL.md new file mode 100644 index 0000000..63d16ba --- /dev/null +++ b/.agent/skills/quality-assurance/pre-write-checklist/SKILL.md @@ -0,0 +1,549 @@ +--- +name: pre-write-checklist +description: "在章节写作前自动激活,强制执行9项必读文件检查清单 - 通过确保每次写作前加载所有上下文来防止AI在长篇小说中的焦点退化" +allowed-tools: Read, Grep +--- + +# 写作前强制检查清单 + +## 核心功能 + +**解决AI长篇失焦问题** - 这是Novel Writer Skills v1.0的核心创新。 + +### 问题根源 + +用户反馈:使用novel-writer创作,前30章质量很好,但30章后AI开始: + +- 忘记前文设定 +- 角色性格不一致 +- 情节重复或矛盾 +- 忽略创作宪法的原则 + +**根本原因**:长对话导致AI遗忘早期context,即使specification.md写得再详细也会被忘记。 + +### 解决方案 + +**每次写作前强制重读所有关键文件** → AI重新加载完整context → 保持一致性 + +--- + +## 9项强制检查清单 + +每次执行`/write`命令时,必须先完成此检查清单: + +```markdown +📋 写作前检查清单(必须完成): + +✓ 1. memory/constitution.md - 创作宪法 +✓ 2. memory/style-reference.md - 风格参考(如有) +✓ 3. stories/_/specification.md - 故事规格 +✓ 4. stories/_/creative-plan.md - 创作计划 +✓ 5. stories/\*/tasks.md - 当前任务 +✓ 6. spec/tracking/character-state.json - 角色状态 +✓ 7. spec/tracking/relationships.json - 关系网络 +✓ 8. spec/tracking/plot-tracker.json - 情节追踪(如有) +✓ 9. spec/tracking/validation-rules.json - 验证规则(如有) + +📊 上下文加载状态:✅ 完成 +``` + +--- + +## 工作原理 + +### 自动触发时机 + +1. **用户执行 `/write` 命令** +2. **本Skill自动激活** +3. **强制执行检查清单** +4. **输出确认报告** +5. **然后才开始写作** + +### 执行流程 + +``` +用户: /write 第10章 + + ↓ + +[pre-write-checklist 自动激活] + + ↓ + +步骤1:读取 memory/constitution.md +步骤2:读取 memory/style-reference.md(如有) +步骤3:读取 stories/*/specification.md +步骤4:读取 stories/*/creative-plan.md +步骤5:读取 stories/*/tasks.md +步骤6:读取 spec/tracking/character-state.json +步骤7:读取 spec/tracking/relationships.json +步骤8:读取 spec/tracking/plot-tracker.json(如有) +步骤9:读取 spec/tracking/validation-rules.json(如有) + + ↓ + +输出确认: +📋 写作前检查清单(已完成): +✓ 1-9 所有文件已读取 +📊 上下文加载状态:✅ 完成 + +关键信息摘要: +- 创作原则:[从constitution提取] +- 当前任务:[从tasks.md提取] +- 主要角色:[从character-state提取] +- 情节进度:[从plot-tracker提取] + + ↓ + +开始写作第10章... +``` + +--- + +## 输出格式 + +### 标准输出(所有文件存在) + +```markdown +📋 写作前检查清单(已完成): + +✓ 1. memory/constitution.md - 创作宪法 +→ 核心原则:[列出2-3条关键原则] + +✓ 2. memory/style-reference.md - 风格参考 +→ 风格要点:[提取关键风格要求] + +✓ 3. stories/xxx/specification.md - 故事规格 +→ 故事类型:[言情/悬疑/历史等] +→ P0元素:[必须包含的元素] + +✓ 4. stories/xxx/creative-plan.md - 创作计划 +→ 当前阶段:[第X卷/第X章] +→ 本章目标:[情节/情感目标] + +✓ 5. stories/xxx/tasks.md - 当前任务 +→ 待写章节:[第X章] +→ 任务状态:[pending/in_progress] + +✓ 6. spec/tracking/character-state.json - 角色状态 +→ 主要角色:[列出角色名和当前状态] + +✓ 7. spec/tracking/relationships.json - 关系网络 +→ 核心关系:[主角与谁的关系变化] + +✓ 8. spec/tracking/plot-tracker.json - 情节追踪 +→ 活跃线索:[当前进行中的情节线] + +✓ 9. spec/tracking/validation-rules.json - 验证规则 +→ 自动修复:[启用/禁用] + +📊 上下文加载状态:✅ 完成(加载9个文件,约XXXX tokens) + +🎯 准备写作第X章... +``` + +### 部分文件缺失时 + +```markdown +📋 写作前检查清单(部分完成): + +✓ 1. memory/constitution.md - 创作宪法 +✓ 2. ⚠️ memory/style-reference.md - 不存在(可选文件,跳过) +✓ 3. stories/xxx/specification.md - 故事规格 +✓ 4. stories/xxx/creative-plan.md - 创作计划 +✓ 5. stories/xxx/tasks.md - 当前任务 +✓ 6. spec/tracking/character-state.json - 角色状态 +✓ 7. spec/tracking/relationships.json - 关系网络 +✓ 8. ⚠️ spec/tracking/plot-tracker.json - 不存在(可选文件,跳过) +✓ 9. ⚠️ spec/tracking/validation-rules.json - 不存在(可选文件,跳过) + +📊 上下文加载状态:✅ 完成(加载6个必须文件 + 0个可选文件) + +💡 建议:运行 `/track-init` 初始化完整追踪系统 +``` + +### 关键文件缺失时(阻止写作) + +```markdown +📋 写作前检查清单(失败): + +✓ 1. memory/constitution.md - 创作宪法 +✓ 2. memory/style-reference.md - 风格参考 +❌ 3. stories/xxx/specification.md - **文件不存在** +❌ 4. stories/xxx/creative-plan.md - **文件不存在** +❌ 5. stories/xxx/tasks.md - **文件不存在** + +⛔ 错误:缺少必需文件,无法继续写作 + +必须先完成: + +1. 运行 `/constitution` 创建创作宪法 +2. 运行 `/specify` 定义故事规格 +3. 运行 `/plan` 制定创作计划 +4. 运行 `/tasks` 分解任务清单 + +然后才能执行 `/write` + +这是seven-step methodology的推荐流程。 +``` + +--- + +## 与Commands集成 + +### `/write` 命令 + +**必须先执行检查清单,才能写作**: + +```yaml +执行顺序: +1. pre-write-checklist(本Skill)→ 读取所有文件 +2. 输出确认报告 +3. 检查setting-detector → 是否需要激活知识库 +4. 开始实际写作 +``` + +### `/analyze` 命令 + +分析时也建议执行检查清单: + +```yaml +分析前先确保context完整: +1. pre-write-checklist → 重新加载所有文件 +2. 基于最新状态执行分析 +``` + +### `/track` 命令 + +追踪更新后触发检查清单: + +```yaml +更新流程: +1. 用户修改tracking文件 +2. 运行 `/track` 更新 +3. pre-write-checklist → 重新读取验证 +``` + +--- + +## 文件重要性分类 + +### 必须文件(缺失则阻止写作) + +``` +1. memory/constitution.md - 创作原则 +3. stories/*/specification.md - 故事规格 +4. stories/*/creative-plan.md - 创作计划 +5. stories/*/tasks.md - 当前任务 +6. spec/tracking/character-state.json - 角色状态 +7. spec/tracking/relationships.json - 关系网络 +``` + +**逻辑**:没有这些文件,AI不知道: + +- 要遵循什么原则 +- 故事是关于什么的 +- 当前写到哪里了 +- 角色是谁、什么状态 + +### 可选文件(缺失时警告但允许继续) + +``` +2. memory/style-reference.md - 风格参考 +8. spec/tracking/plot-tracker.json - 情节追踪 +9. spec/tracking/validation-rules.json - 验证规则 +``` + +**逻辑**:这些文件增强质量,但不是最低要求: + +- style-reference:某些用户不用/book-internalize +- plot-tracker:简单故事可能不需要 +- validation-rules:非必需的自动化 + +--- + +## 防失焦机制 + +### 问题场景 + +``` +第1章写作: +- AI记得所有设定 +- 质量很好 + +第10章写作: +- 对话已经很长 +- AI开始遗忘第1章的设定 + +第30章写作: +- 完全忘记早期设定 +- 角色性格走样 +- 情节自相矛盾 +``` + +### 解决机制 + +``` +每次写作前: +- 强制重读所有核心文件 +- 重新加载完整context +- 像写第1章一样对待第30章 + +结果: +- 第30章质量 ≈ 第1章质量 +- 一致性保持 +- 不再失焦 +``` + +### 效果对比 + +| 对比维度 | 无检查清单 | 有检查清单 | +| --------- | ------------- | ---------- | +| 第1-10章 | ✓ 质量好 | ✓ 质量好 | +| 第11-30章 | ⚠️ 开始不稳定 | ✓ 保持稳定 | +| 第31-50章 | ❌ 明显失焦 | ✓ 依然稳定 | +| 第51+章 | ❌ 严重失焦 | ✓ 长期稳定 | + +--- + +## 配置选项 + +### 调整严格度 + +**默认:严格模式**(推荐) + +``` +"使用严格检查清单模式" +→ 缺少必需文件则阻止写作 +``` + +**宽松模式**(不推荐) + +``` +"使用宽松检查清单模式" +→ 允许跳过部分文件(不推荐,可能失焦) +``` + +### 自定义检查项 + +如果你有额外的重要文件: + +``` +"检查清单请额外包含: +- spec/knowledge/worldbuilding/magic-system.md +- spec/knowledge/characters/protagonist-profile.md" +``` + +--- + +## 性能优化 + +### Token消耗 + +``` +每次写作的额外token成本: + +9个文件读取: +- constitution.md:~200 tokens +- specification.md:~500 tokens +- creative-plan.md:~300 tokens +- tasks.md:~150 tokens +- character-state.json:~200 tokens +- relationships.json:~150 tokens +- 其他:~200 tokens + +总计:约1700 tokens/次写作 + +收益: +- 避免失焦导致的重写(节省数万tokens) +- 保持质量一致(用户满意度) +- 长篇项目的可持续性 +``` + +**ROI极高**:1700 tokens换来长期稳定质量。 + +### 缓存策略 + +``` +同一写作会话中: +第1次写作:读取所有文件(1700 tokens) +第2次写作(1小时内):检查文件是否修改 +- 未修改:使用缓存(0 tokens) +- 已修改:重新读取(部分tokens) +``` + +--- + +## 常见问题 + +### Q: 每次写作都要读这么多文件,会不会很慢? + +**A**: 不会。 + +- 文件读取很快(毫秒级) +- token消耗合理(~1700 tokens) +- 换来的是长期质量保证 + +**对比**: + +- 不用检查清单:第30章质量差 → 用户要求重写10章 → 浪费数万tokens +- 用检查清单:每章+1700 tokens → 50章也只+85000 tokens → 但质量稳定 + +### Q: 我能跳过检查清单吗? + +**A**: 技术上可以,但**强烈不推荐**。 + +``` +"跳过检查清单,直接写作" +→ AI会警告:"不推荐,可能导致失焦" +→ 但会尊重你的选择 +``` + +**后果自负**:30章后失焦了别说我没提醒你😊 + +### Q: 某些文件我确实没有怎么办? + +**A**: 分两种情况: + +**必需文件缺失**(constitution、specification等): +→ 阻止写作,提示先运行对应命令创建 + +**可选文件缺失**(style-reference、plot-tracker): +→ 警告但允许继续,建议后续创建 + +### Q: 检查清单和setting-detector的关系? + +**A**: 互补工作: + +``` +pre-write-checklist: +- 加载项目特定文件(你的故事数据) + +setting-detector: +- 加载通用知识库(类型惯例、写作技巧) + +两者结合 = 完整context: +你的故事设定 + 类型专业知识 +``` + +### Q: 100章的长篇小说也要每次都读吗? + +**A**: 是的,而且**更需要**。 + +``` +长篇小说的挑战: +- 设定更复杂 +- 角色更多 +- 情节线更多 +- AI更容易忘记 + +检查清单的作用: +- 确保第100章和第1章质量一致 +- 防止角色性格突变 +- 防止情节自相矛盾 + +这是长篇小说质量保证的基石。 +``` + +--- + +## 最佳实践 + +### 1. 保持文件更新 + +检查清单只能确保AI读取文件,但文件内容要准确: + +``` +✓ 角色状态变化 → 更新 character-state.json +✓ 关系变化 → 更新 relationships.json +✓ 新情节线 → 更新 plot-tracker.json +``` + +### 2. 定期运行 `/track` + +``` +建议频率:每5-10章运行一次 `/track` +作用: +- 更新tracking文件 +- 验证一致性 +- 发现潜在问题 +``` + +### 3. 重要变更后手动触发 + +``` +如果你手动修改了关键文件: +"请重新执行检查清单,重新加载所有文件" + +确保AI看到最新状态。 +``` + +### 4. 与consistency-checker配合 + +``` +pre-write-checklist(写前): +- 加载所有context +- 准备写作 + +consistency-checker(写中/写后): +- 监控一致性 +- 发现矛盾 +``` + +双重保障 = 最高质量。 + +--- + +## 技术实现 + +### 文件读取顺序 + +``` +优先级排序(重要的先读): +1. constitution(最高原则) +2. specification(故事核心) +3. creative-plan(技术方案) +4. tasks(当前任务) +5. character-state(角色数据) +6. relationships(关系数据) +7. plot-tracker(情节追踪) +8. validation-rules(验证规则) +9. style-reference(风格参考) +``` + +### 错误处理 + +``` +文件不存在: +→ 必需文件:阻止写作,提示创建 +→ 可选文件:警告,允许继续 + +文件格式错误: +→ JSON解析失败:显示错误,建议修复 +→ Markdown格式问题:尽力读取,标记问题 + +文件过大: +→ 超过10000行:警告(可能影响性能) +→ 建议拆分文件 +``` + +--- + +## 总结 + +pre-write-checklist是Novel Writer Skills v1.0的**核心创新**: + +✓ 解决AI长篇失焦问题 +✓ 强制重读关键文件 +✓ 确保context完整性 +✓ 保持质量长期稳定 +✓ 适合专业作者长篇创作 + +**30章后不再失焦 = 长期竞争力** 🎯 + +--- + +**本Skill版本**: v1.0 +**最后更新**: 2025-10-18 +**核心问题**: 解决30章后AI失焦 +**配合**: write.md, setting-detector, consistency-checker diff --git a/.agent/skills/quality-assurance/requirement-detector/CONFLICT_RESOLUTION.md b/.agent/skills/quality-assurance/requirement-detector/CONFLICT_RESOLUTION.md new file mode 100644 index 0000000..5587ed7 --- /dev/null +++ b/.agent/skills/quality-assurance/requirement-detector/CONFLICT_RESOLUTION.md @@ -0,0 +1,131 @@ +# 多规范探测与冲突处理 + +当探测到多个规范关键词时,需要进行冲突检测并提供解决方案。 + +--- + +## 兼容组合示例 + +### fast-paced + anti-ai-v4 + +**用户说**: + +> "我要写爽文,节奏快,而且要去AI味" + +**处理方式**: + +```markdown +🔍 多规范探测结果 + +检测到: + +- fast-paced(因为"爽文、节奏快") +- anti-ai-v4(因为"去AI味") + +这两个规范可以组合使用! + +推荐配置: +writing-requirements: + +- fast-paced +- anti-ai-v4 + +效果:快节奏爽文 + 强力去AI味 = 自然流畅的商业化网文 + +确认应用这个组合吗? +``` + +--- + +## 冲突组合示例 + +### serious-literature + fast-paced + +**用户说**: + +> "我要写严肃文学,但也要每章都有爽点" + +**处理方式**: + +```markdown +⚠️ 规范冲突检测 + +检测到: + +- serious-literature(严肃文学) - 要求慢节奏、深度 +- fast-paced(快节奏爽文) - 要求每章2个爽点 + +这两个规范有根本冲突: + +- 严肃文学:慢节奏、思想性、开放结局 +- 快节奏爽文:密集冲突、爽感优先、套路化 + +建议: + +1. 选择 serious-literature,放弃"每章爽点"要求 +2. 选择 fast-paced,调整为"有深度的爽文" +3. 考虑使用 strong-emotion(强情绪)作为折中 + +你倾向哪个方向? +``` + +--- + +### romance-sweet + romance-angst + +**用户说**: + +> "我要写甜文,但也要虐一下" + +**处理方式**: + +```markdown +⚠️ 甜虐冲突检测 + +检测到: + +- romance-sweet(甜文) - 禁止长时间虐 +- romance-angst(虐文) - 需要虐心情节 + +建议处理方式: + +1. **先甜后虐**(虐恋) + - 前半部分:romance-sweet + - 后半部分:romance-angst + +2. **虐中带甜**(微虐) + - 主要:romance-sweet + - 允许:短暂小虐(1-2章) + - 甜虐比例:80% 甜 / 20% 虐 + +3. **BE甜文**(先甜后刀) + - 前期:romance-sweet + - 结局:romance-angst(BE) + +你想要哪种模式? +``` + +--- + +## 冲突矩阵 + +| 组合 | 兼容性 | 说明 | +| ---------------------------------- | ------- | ------------------ | +| anti-ai-v4 + fast-paced | ✅ 兼容 | 极致自然的爽文 | +| anti-ai-v4 + no-poison | ✅ 兼容 | 自然且逻辑合理 | +| anti-ai-v4 + romance-sweet | ✅ 兼容 | 自然的甜文 | +| fast-paced + no-poison | ✅ 兼容 | 合理的爽文 | +| fast-paced + strong-emotion | ✅ 兼容 | 情绪饱满的爽文 | +| serious-literature + fast-paced | ❌ 冲突 | 慢节奏 vs 快节奏 | +| serious-literature + romance-sweet | ⚠️ 谨慎 | 可以尝试,但需平衡 | +| romance-sweet + romance-angst | ❌ 冲突 | 甜 vs 虐 | + +--- + +## 处理原则 + +1. **优先用户意图**:尊重用户明确表达的偏好 +2. **提供选项**:遇到冲突时给出解决方案 +3. **解释原因**:说明为什么冲突 +4. **支持组合**:对于兼容的组合,鼓励使用 +5. **允许调整**:支持中途切换规范 diff --git a/.agent/skills/quality-assurance/requirement-detector/EXAMPLES.md b/.agent/skills/quality-assurance/requirement-detector/EXAMPLES.md new file mode 100644 index 0000000..aabf497 --- /dev/null +++ b/.agent/skills/quality-assurance/requirement-detector/EXAMPLES.md @@ -0,0 +1,153 @@ +# 完整对话示例和成功案例 + +本文档展示 requirement-detector 在实际使用中的完整对话流程。 + +--- + +## 示例 1:基础规范探测 + +**用户**: + +> "我要写现代言情,甜文,但不能有AI味" + +**requirement-detector 响应**: + +``` +📋 规范探测器已激活 + +检测到关键词: +- "甜文" → romance-sweet +- "不能有AI味" → anti-ai-v4 + +建议规范组合: +1. romance-sweet(甜文规范) +2. anti-ai-v4(强力AI去味) + +✅ 已加载规范文档 +✅ 已写入配置: + writing-requirements: + - romance-sweet + - anti-ai-v4 + +配合建议: +💡 检测到"现代言情",建议同时激活 + style-detector 选择 natural-voice 风格。 +``` + +--- + +## 示例 2:与 style-detector 协同 + +**用户**: + +> "我要写都市爽文,口语化,去AI味" + +**system(两个detector协同)**: + +``` +🎨 style-detector 已激活 +检测到:口语化 → natural-voice + +📋 requirement-detector 已激活 +检测到:爽文 → fast-paced +检测到:去AI味 → anti-ai-v4 + +✅ 推荐配置组合: + +风格层:natural-voice +规范层:fast-paced + anti-ai-v4 + +最终效果: +自然口语化的快节奏爽文,强力去AI味 + +已写入配置: +--- +writing-style: natural-voice +writing-requirements: + - fast-paced + - anti-ai-v4 +--- + +这个组合特别适合你的需求,确认应用吗? +``` + +--- + +## 示例 3:冲突处理 + +**用户**: + +> "我要写严肃文学,但也要每章都有爽点" + +**requirement-detector 响应**: + +``` +⚠️ 规范冲突检测 + +检测到: +- serious-literature(因为"严肃文学") +- fast-paced(因为"每章都有爽点") + +这两个规范有根本冲突: +- 严肃文学:慢节奏、思想性 +- 快节奏爽文:密集冲突、爽感优先 + +建议: +1. 选择 serious-literature,调整"爽点"为"情感高潮点" + → 可以使用 strong-emotion 代替 fast-paced +2. 选择 fast-paced,调整为"有深度的爽文" + → 降低爽点密度,增加思想性 + +你倾向哪个方向? +``` + +--- + +## 示例 4:主动建议 + +**用户已经选择了 natural-voice 风格** + +**requirement-detector 主动建议**: + +``` +💡 规范建议 + +检测到你选择了 natural-voice(自然人声风格) + +建议配套规范: +1. **anti-ai-v4** - 与 natural-voice 完美配合,极致自然 +2. **no-poison** - 保证逻辑合理,避免降智 +3. **fast-paced**(可选) - 如果写商业化爽文 + +推荐组合: +natural-voice + anti-ai-v4 + no-poison += 极致自然、逻辑自洽的现代小说 + +需要我加载这些规范吗? +``` + +--- + +## 使用技巧 + +1. **明确需求**:说清楚你要什么效果(爽文、虐文、去AI味等) +2. **信任探测**:探测器会基于经验给出推荐 +3. **组合使用**:多个兼容的规范可以叠加 +4. **注意冲突**:遇到冲突时,选择最重要的需求 +5. **配合风格**:规范(requirement)+ 风格(style)效果更好 + +--- + +## 常见问题 + +**Q:可以同时使用多个规范吗?** +A:可以,但要注意兼容性。参考 [CONFLICT_RESOLUTION.md](CONFLICT_RESOLUTION.md)。 + +**Q:anti-ai-v4 和 anti-ai-v3 有什么区别?** +A:v4 是强力版本,限制更严格;v3 是标准版本,更平衡。 + +**Q:爽文规范会影响文学性吗?** +A:fast-paced 强调节奏和爽点,与 serious-literature 有冲突。但可以和 literary 风格配合。 + +**Q:甜文和虐文能混合吗?** +A:不推荐,但可以分阶段使用(先甜后虐)。详见 CONFLICT_RESOLUTION.md。 diff --git a/.agent/skills/quality-assurance/requirement-detector/KEYWORDS.md b/.agent/skills/quality-assurance/requirement-detector/KEYWORDS.md new file mode 100644 index 0000000..dac7658 --- /dev/null +++ b/.agent/skills/quality-assurance/requirement-detector/KEYWORDS.md @@ -0,0 +1,178 @@ +# 写作规范关键词详细列表 + +本文档包含所有可探测规范的详细关键词列表、触发场景示例和激活后行为模板。 + +--- + +## 1. anti-ai-v4(强力AI去味) + +### 关键词 + +``` +AI味重、AI腔太重、太AI了 +去AI味、去除AI感、不要AI味 +太机器、太生硬、不自然 +AI痕迹明显、一看就是AI写的 +要求极致自然、完全去AI +最强去味、强力去AI +``` + +### 触发场景示例 + +> 用户:"这个AI味道太重了,能不能去掉?" +> 用户:"我要最强的AI去味,完全看不出AI写的那种" + +### 激活后行为模板 + +```markdown +📋 规范探测器已激活 + +检测到:【AI味重、去AI味】 + +建议使用:**anti-ai-v4**(强力AI去味规范) + +这个规范的特点: + +- 200+ 禁用词汇表 +- 6层规则体系(词汇、句式、形容词、成语、对话、段落) +- 极致的自然化要求 + +⚠️ 警告:这是最激进的去AI规范,可能会牺牲部分文学性 + +想要查看详细的规范文档吗? +``` + +--- + +## 2. anti-ai-v3(标准AI去味) + +### 关键词 + +``` +自然一点、不要太生硬 +少一点AI感、适度去AI +正常的去AI就行、标准去味 +不要太明显、平衡一下 +``` + +### 触发场景示例 + +> 用户:"去掉AI味就行,不用太极端" +> 用户:"适度自然一点,保持可读性" + +--- + +## 3. fast-paced(快节奏/爽文规范) + +### 关键词 + +``` +节奏快、快节奏、爽文、爽点 +打脸、装逼、升级、逆袭 +冲突密集、不拖沓、干脆利落 +每章都要有爽点、要爽 +网文节奏、商业化节奏 +``` + +### 触发场景示例 + +> 用户:"我要写爽文,每章都要有爽点" +> 用户:"节奏快一点,不要拖沓" + +--- + +## 4. no-poison(无毒点规范) + +### 关键词 + +``` +无毒点、不要毒点、避免毒 +不要降智、别让主角傻 +不要圣母、不要工具人 +逻辑自洽、合理、不强行 +不要为了剧情牺牲智商 +``` + +### 触发场景示例 + +> 用户:"千万不要降智,主角别太傻" +> 用户:"要合理,不要强行的剧情" + +--- + +## 5. serious-literature(严肃文学规范) + +### 关键词 + +``` +严肃文学、纯文学、文学性 +有深度、思想性、哲思 +现实主义、人性探讨 +不要爽文套路、要有内涵 +社会问题、深刻、有意义 +``` + +### 触发场景示例 + +> 用户:"我要写严肃文学,有思想深度的" +> 用户:"不要爽文那套,要探讨人性" + +--- + +## 6. strong-emotion(强情绪规范) + +### 关键词 + +``` +情绪饱满、情感强烈、有感染力 +情绪爆发、情绪冲击、催泪 +让人有共鸣、打动人心 +有情感张力、情绪起伏大 +``` + +### 触发场景示例 + +> 用户:"我要写得情感饱满,能打动人" +> 用户:"需要强烈的情绪冲击" + +--- + +## 7. romance-sweet(甜文规范) + +### 关键词 + +``` +甜文、甜、撒糖、高糖 +齁甜、甜到发腻、甜宠 +HE、圆满、温馨 +不虐、少虐、轻松 +``` + +### 触发场景示例 + +> 用户:"我要写甜文,多撒糖" +> 用户:"全程高糖,不要虐" + +--- + +## 8. romance-angst(虐文规范) + +### 关键词 + +``` +虐文、虐、BE、刀子 +虐心、虐恋、悲剧 +误会虐、分离虐、时机虐 +重虐、极虐、生离死别 +``` + +### 触发场景示例 + +> 用户:"我要写虐文,虐得痛快" +> 用户:"BE结局,生离死别那种" + +--- + +## 使用说明 + +当用户提到任何上述关键词时,按照对应的激活后行为模板与用户交互。 diff --git a/.agent/skills/quality-assurance/requirement-detector/SKILL.md b/.agent/skills/quality-assurance/requirement-detector/SKILL.md new file mode 100644 index 0000000..213e28e --- /dev/null +++ b/.agent/skills/quality-assurance/requirement-detector/SKILL.md @@ -0,0 +1,157 @@ +--- +name: requirement-detector +description: "探测用户的写作规范需求并加载对应文档。当用户提到AI味重、去AI味、自然、爽文、快节奏、爽点、无毒点、不降智、严肃文学、有深度、强情绪、打动人、甜文、撒糖、虐文、虐心、BE等关键词时自动激活。适用于讨论写作要求、AI去味方法、节奏控制、情感表达时使用。" +allowed-tools: Read, Edit +--- + +# 写作规范探测器 + +## 核心功能 + +自动探测用户的写作规范需求,并无缝加载对应的规范知识库。 + +## 可探测的规范 + +### 1. anti-ai-v4(强力AI去味) + +- **适合**:对AI味零容忍的作品、需要极致口语化的小说 +- **特点**:200+ 禁用词汇表、6层规则体系、极致自然化 +- **触发词**:AI味重、去AI味、太机器、不自然、最强去味 + +### 2. anti-ai-v3(标准AI去味) + +- **适合**:大部分现代小说、需要平衡自然感和文学性 +- **特点**:平衡的去AI策略、适度的词汇限制 +- **触发词**:自然一点、少一点AI感、适度去AI、标准去味 + +### 3. fast-paced(快节奏/爽文规范) + +- **适合**:都市爽文、玄幻升级流、系统文、重生文 +- **特点**:每章至少2个爽点、打脸/装逼/升级公式 +- **触发词**:爽文、快节奏、爽点、打脸、装逼、升级 + +### 4. no-poison(无毒点规范) + +- **适合**:所有类型小说(通用)、重视逻辑性的作品 +- **特点**:避免降智、强行误会、玛丽苏、工具人、逻辑漏洞 +- **触发词**:无毒点、不降智、逻辑自洽、合理、不强行 + +### 5. serious-literature(严肃文学规范) + +- **适合**:现实主义小说、历史小说、社会问题小说 +- **特点**:现实主义手法、思想性和哲学探讨、语言克制 +- **触发词**:严肃文学、纯文学、有深度、思想性、人性探讨 + +### 6. strong-emotion(强情绪规范) + +- **适合**:情感类小说、虐文、需要感染力的作品 +- **特点**:每10-15章至少1个情绪爆发点、波浪式情绪节奏 +- **触发词**:情绪饱满、情感强烈、有感染力、打动人心 + +### 7. romance-sweet(甜文规范) + +- **适合**:言情甜文、现代甜宠、轻松向恋爱故事 +- **特点**:糖分密度配置、冲突控制、禁止BE倾向 +- **触发词**:甜文、撒糖、高糖、齁甜、甜宠、HE + +### 8. romance-angst(虐文规范) + +- **适合**:虐恋小说、悲剧向爱情、刀子文 +- **特点**:虐要有理有度、虐点设计、虐度等级控制 +- **触发词**:虐文、虐心、BE、刀子、分离虐、时机虐 + +详细关键词列表和触发场景请参阅 [KEYWORDS.md](KEYWORDS.md)。 + +--- + +## 激活后的工作流程 + +### 步骤 1:确认规范选择 + +当探测到关键词后,首先确认用户意图: + +``` +我注意到你提到了【AI味重、去AI味】,这通常对应 anti-ai-v4 规范。 + +我可以: +1. 📖 展示这个规范的详细文档 +2. ✅ 直接应用这个规范到你的项目 +3. 🔍 看看其他规范选项 + +你想要哪一个? +``` + +### 步骤 2:加载规范知识库 + +如果用户确认,使用 Read 工具读取对应的知识库文件: + +``` +📖 读取:.claude/knowledge-base/requirements/{requirement-name}.md +``` + +然后展示核心要点,完整规范已加载到上下文中。 + +### 步骤 3:写入配置标记 + +使用 Edit 工具在用户的 `specification.md` 中添加配置: + +```yaml +--- +writing-requirements: + - anti-ai-v4 + - fast-paced +--- +``` + +提示用户配置已保存,在执行 /write 时会自动遵守这些规范。 + +--- + +## 多规范处理 + +当探测到多个规范关键词时,进行兼容性检测: + +- **兼容组合**:自动组合并说明综合效果 +- **冲突组合**:提示矛盾并给出解决方案 + +详细的冲突处理逻辑请参阅 [CONFLICT_RESOLUTION.md](CONFLICT_RESOLUTION.md)。 + +--- + +## 主动建议 + +- **基于风格建议规范**:如果 style-detector 已激活,主动推荐配套规范 +- **中途调整支持**:持续监控用户反馈,支持规范调整 + +--- + +## 与 style-detector 的配合 + +本 Skill 专注于规范层面,与 style-detector(文风层)协同工作: + +``` +用户输入:"我要写都市爽文,口语化,去AI味" + +style-detector → 识别:natural-voice +requirement-detector → 识别:fast-paced + anti-ai-v4 + +最终配置: +writing-style: natural-voice +writing-requirements: + - fast-paced + - anti-ai-v4 + +效果:自然人声的快节奏爽文,强力去AI味 +``` + +--- + +## 使用示例 + +完整的对话示例和成功案例请参阅 [EXAMPLES.md](EXAMPLES.md)。 + +--- + +**Skill 版本**:v1.1 +**最后更新**:2025-10-19 +**兼容版本**:novel-writer-skills v1.0.5+ diff --git a/.agent/skills/quality-assurance/setting-detector/SKILL.md b/.agent/skills/quality-assurance/setting-detector/SKILL.md new file mode 100644 index 0000000..3991fcd --- /dev/null +++ b/.agent/skills/quality-assurance/setting-detector/SKILL.md @@ -0,0 +1,597 @@ +--- +name: setting-detector +description: "基于关键词自动检测故事设定(类型、时代、主题)并激活相应的知识库 - 在后台静默工作,无需用户干预即可提供相关写作指导" +allowed-tools: Read +--- + +# 故事设定自动检测器 + +## 核心功能 + +**自动激活知识库系统** - 这是Novel Writer Skills的核心竞争力。 + +当你提到特定关键词时,我会自动: + +1. 检测故事的类型、时代、主题 +2. 加载对应的写作知识库 +3. 在整个创作过程中应用专业知识 + +**无需手动调用** - 完全自动化,后台运行。 + +--- + +## 工作原理 + +### 关键词映射表 + +我监听以下关键词并自动激活对应知识库: + +#### 类型知识库(Genres) + +**言情小说**(romance): + +``` +触发词:言情、爱情、恋爱、浪漫、感情线、关系弧、CP、甜文、虐文、 + HE、BE、双洁、破镜重圆、先婚后爱、契约关系 + +激活:templates/knowledge-base/genres/romance.md +``` + +**悬疑推理**(mystery): + +``` +触发词:悬疑、推理、侦探、破案、谜团、线索、真相、凶手、犯罪、 + 密室、诡计、不在场证明、推理小说 + +激活:templates/knowledge-base/genres/mystery.md +``` + +**历史小说**(historical): + +``` +触发词:历史、古代、朝代、考据、时代背景、历史小说、古言、 + 穿越、重生古代、架空历史、宫斗、宅斗 + +激活:templates/knowledge-base/genres/historical.md +``` + +**复仇爽文**(revenge): + +``` +触发词:复仇、报仇、打脸、爽文、逆袭、反击、重生复仇、 + 穿越复仇、系统、金手指、女主爽文、男主爽文 + +激活:templates/knowledge-base/genres/revenge.md +``` + +**武侠小说**(wuxia): + +``` +触发词:武侠、江湖、武功、侠客、门派、武学、剑客、 + 轻功、内功、武林、江湖恩仇、侠义 + +激活:templates/knowledge-base/genres/wuxia.md +``` + +#### 参考资料库(References) + +**1920年代中国**(china-1920s): + +``` +触发词:1920、民国、军阀、北洋、穿越民国、二十年代、 + 民国时期、军阀混战 + +激活:templates/knowledge-base/references/china-1920s/ +``` + +--- + +## 自动激活流程 + +### 示例1:单一类型检测 + +``` +用户:"我要写一部言情小说" + ↓ +[检测到关键词:"言情"] + ↓ +✓ 自动加载:romance.md + ↓ +AI回复:"太好了!让我帮你创作言情小说。 +根据言情类型惯例,我们需要明确几个核心元素... +(自动应用romance.md中的知识)" +``` + +### 示例2:多类型组合检测 + +``` +用户:"我要写一部1920年代的言情复仇小说" + ↓ +[检测到关键词:"1920"、"言情"、"复仇"] + ↓ +✓ 自动加载:romance.md +✓ 自动加载:revenge.md +✓ 自动加载:references/china-1920s/ + ↓ +📚 已激活知识库: +- genres/romance.md(言情小说惯例) +- genres/revenge.md(复仇爽文技巧) +- references/china-1920s/(1920年代背景) + ↓ +AI回复:"很好的组合!这是浪漫悬疑+复仇+民国背景。 +根据这三个类型的融合,建议... +(同时应用三个知识库的内容)" +``` + +### 示例3:创作过程中的持续应用 + +``` +/constitution 阶段: +→ 已激活:romance.md +→ 提醒:言情小说需要HEA/HFN结局承诺 + +/specify 阶段: +→ 已激活:romance.md + revenge.md +→ 建议:定义关系弧线+复仇目标 + +/plan 阶段: +→ 已激活:所有知识库 +→ 应用:情感节奏点+打脸节奏+1920年代细节 + +/write 阶段: +→ 已激活:所有知识库 +→ 实时:对话技巧+场景描写+时代氛围 + +/analyze 阶段: +→ 已激活:所有知识库 +→ 检查:言情惯例+复仇合理性+历史准确性 +``` + +--- + +## Token效率优化 + +**为什么这个系统高效?** + +传统方案(superpowers-skills模式): + +``` +50个独立Skills × 每个40行SKILL.md = 2000 tokens +每次对话都加载所有Skills +``` + +我们的方案(分层知识库+检测器): + +``` +1个detector Skill(本文件):~100 tokens +按需加载知识库: +- romance.md:~520 tokens +- revenge.md:~480 tokens +- china-1920s/:~650 tokens + +总计:100 + 520 + 480 + 650 = ~1750 tokens +(仅加载相关知识库) +``` + +**节省75%** vs 加载50个无关Skills + +--- + +## 手动控制 + +### 查看当前激活的知识库 + +随时询问: + +``` +"当前激活了哪些知识库?" +``` + +我会回复: + +``` +📚 当前激活的知识库: +✓ genres/romance.md - 言情小说创作惯例 +✓ genres/revenge.md - 复仇爽文打脸技巧 +✓ references/china-1920s/ - 1920年代中国参考资料 + +Token消耗:约1650 tokens +``` + +### 手动激活知识库 + +如果自动检测失败,可以明确指定: + +``` +"请加载 romance 和 mystery 知识库" +"这个故事需要 1920s 中国参考资料" +"激活武侠知识库" +``` + +### 停用知识库 + +如果某个知识库不需要了: + +``` +"停用 revenge 知识库,这个故事不涉及复仇" +``` + +--- + +## 与Commands集成 + +### `/constitution` - 类型特定原则 + +根据激活的知识库,提供类型特定的创作原则: + +```markdown +【自动插入到constitution中】 + +## 类型特定原则(由setting-detector自动添加) + +### 言情小说核心原则 + +1. 关系弧线是主情节 +2. HEA/HFN结局是必须的 +3. 化学反应真实可信 + +### 复仇爽文核心原则 + +1. 仇恨必须深刻具体 +2. 打脸节奏渐进式 +3. 道德平衡:罪有应得 +``` + +### `/specify` - 类型特定规格 + +提供对应的规格模板元素: + +```markdown +【自动提示】 + +💡 根据已激活的知识库,建议在规格中定义: + +【来自romance.md】 + +- 关系弧线设定 +- 情感节奏点规划 +- HEA/HFN结局类型 + +【来自revenge.md】 + +- 仇恨来源(具体罪行) +- 金手指设定 +- 打脸节奏规划 + +【来自china-1920s】 + +- 具体年份(1920-1929) +- 地理位置(北京/上海/其他) +- 社会阶层设定 +``` + +### `/plan` - 类型知识融合 + +将多个知识库的建议融合到计划中: + +```markdown +【自动应用】 + +## 章节规划(融合3个知识库) + +第1-5章:相遇与仇恨建立 + +- [romance] 初次相遇场景(meet-cute) +- [revenge] 展示前世/过去的深仇大恨 +- [1920s] 民国时代氛围营造 + +第6-10章:关系升温与小打脸 + +- [romance] 暧昧期:肢体接触、情感积累 +- [revenge] 第一次打脸:仇人惊讶 +- [1920s] 服饰、礼仪、社会规则展现 + +... +``` + +### `/write` - 实时写作指导 + +写作时自动应用相关知识: + +```markdown +【写作前自动提醒】 + +📋 本章应用的知识库: + +✓ romance.md - 对话火花技巧 +→ 调情对话:挑战而非顺从 +→ 肢体语言:眼神、触碰、距离 + +✓ revenge.md - 打脸场景爽感 +→ 期待 → 反转 → 惊愕 → 霸气 + +✓ 1920s - 时代细节 +→ 称呼:大人、老爷、小姐 +→ 场景:茶馆、洋行、租界 +``` + +### `/analyze` - 类型惯例检查 + +根据激活的知识库执行对应检查: + +```markdown +【自动分析】 + +## 类型符合度分析 + +### 言情小说检查(基于romance.md) + +- [x] 关系弧线是主情节 +- [x] 包含必备情感节奏点 +- [ ] ⚠️ 缺少初吻场景(建议在第15章) +- [x] HEA结局承诺明确 + +### 复仇爽文检查(基于revenge.md) + +- [x] 仇恨深刻具体 +- [x] 打脸频率合理(每5章1次) +- [x] 主角实力提升合理 + +### 历史准确性检查(基于1920s) + +- [x] 时代背景正确 +- [ ] ⚠️ 第8章出现"手机"(时代不符) +- [x] 称呼系统正确 +``` + +--- + +## 知识库扩展 + +### 当前支持的知识库 + +| 类别 | 已完成 | 计划中 | +| -------- | ------ | ------ | +| 类型知识 | 5个 | 10+ | +| 参考资料 | 1个 | 20+ | + +**已完成**(v1.0): + +- genres/romance.md +- genres/mystery.md +- genres/historical.md +- genres/revenge.md +- genres/wuxia.md +- references/china-1920s/ + +**计划中**(v1.1+): + +- genres/fantasy.md(奇幻) +- genres/sci-fi.md(科幻) +- genres/horror.md(恐怖) +- references/ancient-china/(各朝代) +- references/modern-workplace/(现代职场) + +### 添加新知识库 + +1. 在`templates/knowledge-base/`对应目录创建文件 +2. 更新`templates/knowledge-base/README.md`的关键词映射表 +3. setting-detector会自动识别(无需修改本文件) + +**示例**:添加奇幻知识库 + +```bash +# 1. 创建文件 +touch templates/knowledge-base/genres/fantasy.md + +# 2. 更新README.md关键词映射 +fantasy: + keywords: [奇幻, 魔法, 世界构建, 魔法系统] + auto_load: genres/fantasy.md + +# 3. 完成!下次用户说"奇幻"就会自动激活 +``` + +--- + +## 智能特性 + +### 1. 模糊匹配 + +即使不是精确关键词也能识别: + +``` +"我想写个穿越到民国的复仇故事" + ↓ +识别:"民国" → 1920s中国 + "复仇" → revenge + +"女主重生后要报仇" + ↓ +识别:"重生" + "报仇" → revenge(重生复仇) +``` + +### 2. 上下文理解 + +根据对话上下文持续识别: + +``` +第1条消息:"我要写小说" +→ 未激活任何知识库(等待更多信息) + +第2条消息:"主角是侦探" +→ 激活mystery.md + +第3条消息:"还有感情线" +→ 额外激活romance.md + +→ 当前激活:mystery + romance(浪漫悬疑) +``` + +### 3. 自动去重 + +避免重复激活: + +``` +用户:"这是武侠小说,江湖背景,有武功" + ↓ +识别到3个武侠关键词,但只激活1次wuxia.md +``` + +--- + +## 常见问题 + +### Q: 我怎么知道哪个知识库被激活了? + +**A**: 随时问我"当前激活了哪些知识库?",我会列出清单。 + +### Q: 自动检测错了怎么办? + +**A**: 直接告诉我: + +``` +"这不是言情小说,请停用romance知识库" +"这是科幻小说,请激活sci-fi知识库"(如果已有) +``` + +### Q: 能同时激活多少个知识库? + +**A**: 理论上无限,但建议3-5个最优: + +- 太少:指导不够全面 +- 太多:token消耗大,可能冲突 + +实际经验:3个知识库(如romance + revenge + 1920s)已经很丰富。 + +### Q: 知识库会冲突吗? + +**A**: 不会!知识库设计为协同工作: + +- romance + mystery = 浪漫悬疑✓ +- fantasy + romance = 奇幻言情✓ +- historical + revenge = 古代复仇✓ + +### Q: 什么时候应该手动指定知识库? + +**A**: 当: + +- 使用了非常专业的术语(我可能没识别) +- 想尝试不同类型的融合 +- 自动检测和你的意图不符 + +### Q: 知识库会一直激活吗? + +**A**: 是的,直到: + +- 你明确说"停用XX知识库" +- 或开始新的对话 +- 或切换到完全不同的故事 + +--- + +## 最佳实践 + +### 1. 在创作初期明确类型 + +``` +推荐做法: +用户:"我要写一部民国背景的复仇言情小说" + +不推荐: +用户:"我要写小说" +AI:"好的" +用户:"主角姓李" +AI:"嗯" +(20轮对话后才提到是什么类型) +``` + +### 2. 定期检查激活状态 + +每5-10章问一次: + +``` +"当前激活的知识库还适合吗?" +``` + +我会检查并建议是否需要调整。 + +### 3. 类型融合要合理 + +``` +✓ 好的组合: +- romance + mystery(浪漫悬疑) +- historical + romance(古言) +- revenge + romance(复仇+感情) + +⚠️ 困难的组合: +- horror + romance(恐怖言情?读者可能不适应) +- mystery + wuxia(推理武侠,需要特殊设计) + +不是不能做,但需要特别小心融合方式。 +``` + +### 4. 利用参考资料 + +如果故事有明确时代背景,一定激活对应参考资料: + +``` +"1920年代" → china-1920s +"古代" → 指定具体朝代(如"唐代"、"明朝") +``` + +历史细节准确,读者沉浸感强。 + +--- + +## 技术说明 + +### 检测算法 + +``` +1. 用户输入 → 提取关键词 +2. 关键词 → 映射到知识库(基于README.md) +3. 去重 → 避免重复激活 +4. 加载 → Read相应的.md文件 +5. 应用 → 在后续对话中持续应用知识 +``` + +### Token管理 + +``` +激活状态检查:每次对话开始时 +知识库内容:按需加载,缓存在对话上下文中 +更新策略:只在明确需要时重新读取 +``` + +### 与其他Skills协作 + +setting-detector是"知识调度器",其他Skills是"执行者": + +``` +setting-detector → 决定加载哪些知识 +consistency-checker → 基于激活的知识库检查一致性 +workflow-guide → 根据类型提供特定流程指导 +genre-specific skills → 提供类型特定的深度技巧 +``` + +--- + +## 总结 + +setting-detector是Novel Writer Skills的**大脑**: + +✓ 自动检测故事设定 +✓ 智能激活对应知识库 +✓ Token高效(按需加载) +✓ 持续应用专业知识 +✓ 无需用户干预 + +**让AI自动成为你故事类型的专家!** 🧠✨ + +--- + +**本Skill版本**: v1.0 +**最后更新**: 2025-10-18 +**依赖**: templates/knowledge-base/ 系统 +**协作**: consistency-checker, workflow-guide, genre-specific skills diff --git a/.agent/skills/quality-assurance/style-detector/CONFLICT_RESOLUTION.md b/.agent/skills/quality-assurance/style-detector/CONFLICT_RESOLUTION.md new file mode 100644 index 0000000..395d0e5 --- /dev/null +++ b/.agent/skills/quality-assurance/style-detector/CONFLICT_RESOLUTION.md @@ -0,0 +1,140 @@ +# 多风格探测与冲突处理 + +当探测到多个风格关键词时,需要进行冲突检测并提供解决方案。 + +--- + +## 场景 1:兼容的组合 + +### 示例:natural-voice + literary + +**用户说**: + +> "我要写都市小说,口语化一点,但也要有点文学性" + +**探测结果**: + +- natural-voice(口语化) +- literary(文学性) + +**处理方式**: + +```markdown +🔍 多风格探测结果 + +检测到: + +- natural-voice(因为"口语化") +- literary(因为"文学性") + +这两个风格可以混合使用! + +建议方案: + +1. 主风格:natural-voice (70%) + 辅助:literary (30%) + - 对话和日常用口语化 + - 关键时刻加入文学化描写 + +2. 主风格:literary (60%) + 辅助:natural-voice (40%) + - 整体保持文学性 + - 对话部分口语化 + +你倾向哪种比例? +``` + +--- + +## 场景 2:冲突的组合 + +### 示例:minimal + literary + +**用户说**: + +> "我要极简风格,但也要华丽的辞藻" + +**探测结果**: + +- minimal(极简) +- literary(华丽)- 冲突! + +**处理方式**: + +```markdown +⚠️ 风格冲突检测 + +检测到: + +- minimal(极简风格) - 要求删除所有修饰 +- literary(文学风格) - 需要优美的语言 + +这两个风格有冲突: + +- 极简要求:删除形容词、极短句、零修饰 +- 文学要求:精准词汇、美感、适度修饰 + +建议: + +1. 选择 minimal,放弃华丽辞藻 +2. 选择 literary,调整"极简"为"简洁" +3. 考虑使用 natural-voice(介于两者之间) + +你倾向哪个方向? +``` + +--- + +## 冲突矩阵 + +| 组合 | 兼容性 | 处理方式 | +| ----------------------------- | ------- | -------------------------- | +| natural-voice + literary | ✅ 兼容 | 提供比例混合方案 | +| natural-voice + web-novel | ✅ 兼容 | 口语化网文,推荐 | +| natural-voice + ancient-style | ⚠️ 谨慎 | 现代口语 vs 古韵,需要平衡 | +| natural-voice + minimal | ⚠️ 谨慎 | 都强调简洁,可以组合 | +| literary + web-novel | ⚠️ 谨慎 | 文学性 vs 爽文套路,需取舍 | +| literary + ancient-style | ✅ 兼容 | 都重视意境,推荐 | +| literary + minimal | ❌ 冲突 | 华丽 vs 极简,矛盾 | +| web-novel + ancient-style | ⚠️ 谨慎 | 快节奏 vs 古韵留白,需平衡 | +| web-novel + minimal | ❌ 冲突 | 套路化 vs 极简主义,矛盾 | +| ancient-style + minimal | ⚠️ 谨慎 | 意象 vs 克制,可以尝试 | + +--- + +## 中途调整风格 + +即使项目已经开始,仍然持续监控用户反馈。 + +### 场景:中途切换风格 + +**用户在第10章时说**: + +> "这几章写得太文艺了,我想换成口语化一点" + +**探测器激活**: + +```markdown +🎨 风格调整探测 + +检测到你想调整风格: +当前风格:literary +建议新风格:natural-voice + +这会影响后续章节的写作。 + +调整建议: + +1. 从下一章开始使用新风格 +2. 不修改已写章节(保持一致性) + +确认调整吗? +``` + +--- + +## 处理原则 + +1. **优先用户意图**:当用户明确表达偏好时,尊重用户选择 +2. **提供选项**:遇到冲突时,给出2-3个解决方案 +3. **解释原因**:说明为什么冲突,帮助用户理解 +4. **支持混合**:对于兼容的组合,提供比例建议 +5. **允许调整**:支持中途切换,但提醒影响范围 diff --git a/.agent/skills/quality-assurance/style-detector/EXAMPLES.md b/.agent/skills/quality-assurance/style-detector/EXAMPLES.md new file mode 100644 index 0000000..9f8567d --- /dev/null +++ b/.agent/skills/quality-assurance/style-detector/EXAMPLES.md @@ -0,0 +1,218 @@ +# 完整对话示例和成功案例 + +本文档展示 style-detector 在实际使用中的完整对话流程。 + +--- + +## 示例 1:基础风格探测 + +**用户**: + +> "我要写一个都市爽文,要口语化,真实一点" + +**style-detector 响应**: + +``` +🎨 风格探测器已激活 + +检测到关键词: +- "口语化" → natural-voice +- "真实" → natural-voice + +建议风格:**natural-voice**(自然人声) + +这个风格的特点: +- 以口语化表达为主,像讲故事给朋友听 +- 生活细节丰富,代入感强 +- 对话推动情节(占比 40-60%) +- 避免文艺腔和书面语 + +想要查看详细的风格指南吗? +``` + +**用户**: + +> "好的,应用这个风格" + +**style-detector 执行**: + +``` +✅ 正在加载风格知识库... + +📖 读取:.claude/knowledge-base/styles/natural-voice.md + +【展示核心要点】 + +## 核心原则: +1. 口语化优先 - 能用5个字说清楚的,不用10个字 +2. 对话推动情节 - 对话占比 40-60% +3. 行为>心理 - 多写做了什么,少写想了什么 +4. 具体>抽象 - 用具体的细节,不要抽象的概念 + +## AI 去味要点: +- 禁用词:然而、殊不知、五味杂陈、极其... +- 形容词限制:每100字最多5个 +- 成语使用:能不用就不用 + +✅ 已将风格配置写入 specification.md + +在执行 /write 时,我会自动严格遵守 natural-voice 的所有规范。 + +💡 配合建议: +检测到你也提到了"爽文",建议同时激活 requirement-detector 加载 fast-paced 规范。 +``` + +--- + +## 示例 2:主动建议风格 + +**用户在 specification.md 中写道**: + +> "类型:都市现实主义小说,讲述普通人的生活" + +**style-detector 主动建议**: + +``` +💡 风格建议 + +根据你的故事类型(都市现实主义,普通人生活), +建议使用:**natural-voice**(自然人声风格) + +理由: +✅ 现实题材适合口语化 +✅ 普通人生活需要真实感 +✅ 代入感强 + +当然,这只是建议,你也可以选择其他风格。 +要应用 natural-voice 风格吗? +``` + +--- + +## 示例 3:风格混合 + +**用户**: + +> "我要写历史小说,要有古风的味道,但也要保持一定的文学性" + +**style-detector 响应**: + +``` +🔍 多风格探测结果 + +检测到: +- ancient-style(因为"古风的味道") +- literary(因为"文学性") + +这两个风格可以很好地组合! + +建议方案: +1. 主风格:ancient-style (60%) + 辅助:literary (40%) + - 用古韵营造氛围 + - 用文学性加深意境 + +2. 主风格:literary (55%) + 辅助:ancient-style (45%) + - 以文学性为基础 + - 加入古风元素点缀 + +推荐:方案1 更适合历史小说 + +想应用这个组合吗? +``` + +--- + +## 示例 4:中途调整 + +**项目进行到第10章时,用户说**: + +> "这几章太文艺了,读者反馈说看不懂,能不能换成简单点的" + +**style-detector 响应**: + +``` +🎨 风格调整探测 + +检测到你想调整风格: +当前风格:literary +建议新风格:natural-voice + +这会影响后续章节的写作风格。 + +调整建议: +1. 从第11章开始使用 natural-voice +2. 不修改第1-10章(保持前文一致性) + +这样做的好处: +✅ 后续章节更易读 +✅ 保持已写内容完整性 + +这样做的风险: +⚠️ 前后风格会有明显变化 +⚠️ 读者可能感到文风转变 + +确认调整吗?还是想要更平滑的过渡方案? +``` + +--- + +## 示例 5:与 requirement-detector 协同 + +**用户**: + +> "我要写现代言情,甜甜的那种,要口语化,还要去掉AI味" + +**system(两个detector协同)**: + +``` +🎨 style-detector 已激活 +检测到:口语化 → natural-voice + +📋 requirement-detector 已激活 +检测到:甜甜的 → romance-sweet +检测到:去掉AI味 → anti-ai-v4 + +✅ 推荐配置组合: + +风格层:natural-voice +规范层:romance-sweet + anti-ai-v4 + +最终效果: +自然口语化的甜宠现代言情,强力去AI味 + +已写入配置: +--- +writing-style: natural-voice +writing-requirements: + - romance-sweet + - anti-ai-v4 +--- + +这个组合特别适合你的需求,确认应用吗? +``` + +--- + +## 使用技巧 + +1. **关键词要明确**:直接说"口语化"、"网文"、"古风"等触发词 +2. **描述具体需求**:说明故事类型、目标读者 +3. **信任建议**:探测器会基于经验给出推荐 +4. **勇于调整**:写作过程中随时可以调整风格 +5. **组合使用**:风格(style)+ 规范(requirement)组合效果更好 + +--- + +## 常见问题 + +**Q:可以同时使用多个风格吗?** +A:可以,但要注意兼容性。参考 [CONFLICT_RESOLUTION.md](CONFLICT_RESOLUTION.md) 查看兼容矩阵。 + +**Q:中途能换风格吗?** +A:可以,但建议从新章节开始,保持已写内容的一致性。 + +**Q:不确定用哪个风格怎么办?** +A:描述你的故事类型和目标读者,探测器会主动建议。 + +**Q:风格和规范有什么区别?** +A:风格(style)控制文风和语感,规范(requirement)控制写作标准和要求。两者可以组合使用。 diff --git a/.agent/skills/quality-assurance/style-detector/KEYWORDS.md b/.agent/skills/quality-assurance/style-detector/KEYWORDS.md new file mode 100644 index 0000000..99260c9 --- /dev/null +++ b/.agent/skills/quality-assurance/style-detector/KEYWORDS.md @@ -0,0 +1,227 @@ +# 写作风格关键词详细列表 + +本文档包含所有可探测风格的详细关键词列表、触发场景示例和激活后行为模板。 + +--- + +## 1. natural-voice(自然人声风格) + +### 关键词(任意一个即触发) + +``` +口语化、生活化、真实感、接地气 +像说话一样、不要文艺腔、去掉书面语 +自然、不装、白话、日常对话 +真实、贴近生活、不做作 +``` + +### 触发场景示例 + +> 用户:"我想要口语化一点的风格,不要太文艺" +> 用户:"写得真实一点,像平时说话那样" + +### 激活后行为模板 + +```markdown +🎨 风格探测器已激活 + +我注意到你提到了【口语化、真实感】等关键词。 + +建议使用:**natural-voice**(自然人声风格) + +这个风格的特点: + +- 以口语化表达为主,像讲故事给朋友听 +- 生活细节丰富,代入感强 +- 对话推动情节(占比 40-60%) +- 避免文艺腔和书面语 + +这个风格特别适合: +✅ 都市现代小说 +✅ 现实题材 +✅ 情感类小说 + +想要查看详细的风格指南吗? +``` + +--- + +## 2. literary(文学风格) + +### 关键词 + +``` +文学性、优美、深刻、有深度 +严肃文学、纯文学、意境、诗意(现代文) +哲思、人性、思想性、文艺 +有内涵、有深度、不肤浅 +``` + +### 触发场景示例 + +> 用户:"想写严肃文学,有深度的那种" +> 用户:"要有文学性,不是网文那种" + +### 激活后行为模板 + +```markdown +🎨 风格探测器已激活 + +检测到:【严肃文学、有深度】 + +建议使用:**literary**(文学风格) + +这个风格的特点: + +- 优美、深刻、有文学性 +- 注重意境和哲思 +- 适度的心理描写 +- 语言克制但有力量 + +这个风格特别适合: +✅ 严肃文学作品 +✅ 历史小说(现代背景) +✅ 需要思想深度的故事 + +想要查看详细的风格指南吗? +``` + +--- + +## 3. web-novel(网文风格) + +### 关键词 + +``` +网文、爽文、快节奏、套路 +打脸、装逼、升级流、废柴流 +玄幻风格、仙侠风格、系统文 +爽点、冲突密集 +``` + +### 触发场景示例 + +> 用户:"就是那种网文的感觉" +> 用户:"节奏快一点,爽文那种" + +### 激活后行为模板 + +```markdown +🎨 风格探测器已激活 + +检测到:【网文、爽文、快节奏】 + +建议使用:**web-novel**(网文风格) + +这个风格的特点: + +- 节奏快,冲突密集 +- 爽点频繁(打脸、装逼、升级) +- 对话简短有力 +- 套路清晰但不老套 + +这个风格特别适合: +✅ 玄幻小说 +✅ 都市爽文 +✅ 系统流、重生流 + +想要查看详细的风格指南吗? +``` + +--- + +## 4. ancient-style(古风) + +### 关键词 + +``` +古风、古韵、文言(不要太重) +诗意、意象、留白、武侠风 +古典美、传统、江湖味 +有古韵、古代感、古典 +``` + +### 触发场景示例 + +> 用户:"想要古风的文字" +> 用户:"要有武侠小说那种味道" + +### 激活后行为模板 + +```markdown +🎨 风格探测器已激活 + +检测到:【古风、武侠风】 + +建议使用:**ancient-style**(古风) + +这个风格的特点: + +- 有古韵,但不是纯文言 +- 意象丰富,留白艺术 +- 诗意化的描写 +- 避免现代化词汇 + +这个风格特别适合: +✅ 武侠小说 +✅ 仙侠(文风层面) +✅ 古代背景小说 + +想要查看详细的风格指南吗? +``` + +--- + +## 5. minimal(极简风格) + +### 关键词 + +``` +极简、简洁、克制、海明威式 +冷硬、少废话、不啰嗦、留白 +不解释、硬汉风格、简约 +``` + +### 触发场景示例 + +> 用户:"简洁一点,不要那么多描写" +> 用户:"海明威那种风格" + +### 激活后行为模板 + +```markdown +🎨 风格探测器已激活 + +检测到:【极简、海明威式】 + +建议使用:**minimal**(极简风格) + +这个风格的特点: + +- 高度简洁,删除所有修饰 +- 短句为主,节奏硬朗 +- 留白,不解释 +- 海明威式克制 + +⚠️ 警告:这个风格非常极端,不适合所有人 + +这个风格特别适合: +✅ 悬疑推理 +✅ 冷硬派小说 +✅ 实验性作品 + +想要查看详细的风格指南吗? +``` + +--- + +## 使用说明 + +当用户提到任何上述关键词时,按照对应的激活后行为模板与用户交互: + +1. 确认探测到的关键词 +2. 建议对应的风格 +3. 简要说明特点和适用场景 +4. 询问是否查看详细指南 + +如果用户确认,继续执行加载知识库和写入配置的步骤。 diff --git a/.agent/skills/quality-assurance/style-detector/SKILL.md b/.agent/skills/quality-assurance/style-detector/SKILL.md new file mode 100644 index 0000000..3658b49 --- /dev/null +++ b/.agent/skills/quality-assurance/style-detector/SKILL.md @@ -0,0 +1,131 @@ +--- +name: style-detector +description: "探测用户的写作风格需求并加载对应指南。当用户提到口语化、生活化、真实感、文学性、严肃文学、纯文学、网文、爽文、快节奏、古风、武侠、古韵、极简、海明威、克制等关键词时自动激活。适用于讨论小说风格、写作文风、创作方向时使用。" +allowed-tools: Read, Edit +--- + +# 写作风格探测器 + +## 核心功能 + +自动探测用户的写作风格需求,并无缝加载对应的风格知识库。 + +## 可探测的风格 + +### 1. natural-voice(自然人声风格) + +- **适合**:都市现代小说、现实题材、情感类小说 +- **特点**:口语化、生活化、真实感强 +- **触发词**:口语化、真实感、接地气、像说话一样 + +### 2. literary(文学风格) + +- **适合**:严肃文学作品、历史小说、需要思想深度的故事 +- **特点**:优美、深刻、有文学性、注重意境 +- **触发词**:文学性、严肃文学、纯文学、有深度 + +### 3. web-novel(网文风格) + +- **适合**:玄幻小说、都市爽文、系统流、重生流 +- **特点**:节奏快、冲突密集、爽点频繁 +- **触发词**:网文、爽文、打脸、装逼、升级流 + +### 4. ancient-style(古风) + +- **适合**:武侠小说、仙侠、古代背景小说 +- **特点**:有古韵但不纯文言、意象丰富、诗意化 +- **触发词**:古风、古韵、武侠风、江湖味 + +### 5. minimal(极简风格) + +- **适合**:悬疑推理、冷硬派小说、实验性作品 +- **特点**:高度简洁、海明威式克制、留白艺术 +- **触发词**:极简、海明威、克制、冷硬 + +详细关键词列表和触发场景请参阅 [KEYWORDS.md](KEYWORDS.md)。 + +--- + +## 激活后的工作流程 + +### 步骤 1:确认风格选择 + +当探测到关键词后,首先确认用户意图: + +``` +我注意到你提到了【口语化】,这通常对应 natural-voice 风格。 + +我可以: +1. 📖 展示这个风格的详细指南 +2. ✅ 直接应用这个风格到你的项目 +3. 🔍 看看其他风格选项 + +你想要哪一个? +``` + +### 步骤 2:加载风格知识库 + +如果用户确认,使用 Read 工具读取对应的知识库文件: + +``` +📖 读取:.claude/knowledge-base/styles/{style-name}.md +``` + +然后展示核心要点,完整规范已加载到上下文中。 + +### 步骤 3:写入配置标记 + +使用 Edit 工具在用户的 `specification.md` 中添加配置: + +```yaml +--- +writing-style: natural-voice +--- +``` + +提示用户配置已保存,在执行 /write 时会自动遵守该风格规范。 + +--- + +## 多风格处理 + +当探测到多个风格关键词时,进行兼容性检测: + +- **兼容组合**:提供混合方案和比例建议 +- **冲突组合**:提示矛盾并给出解决方案 + +详细的冲突处理逻辑请参阅 [CONFLICT_RESOLUTION.md](CONFLICT_RESOLUTION.md)。 + +--- + +## 主动建议 + +- **基于故事类型建议风格**:分析 specification 中的故事类型,主动推荐适合的风格 +- **中途调整支持**:持续监控用户反馈,支持风格调整 + +--- + +## 与 requirement-detector 的配合 + +本 Skill 专注于文风层面,与 requirement-detector(规范层)协同工作: + +``` +style-detector → 识别文风 → 加载 styles/*.md +requirement-detector → 识别规范 → 加载 requirements/*.md + +最终组合效果示例: +natural-voice (风格) + anti-ai-v4 + fast-paced (规范) += 自然人声的快节奏爽文,强力去AI味 +``` + +--- + +## 使用示例 + +完整的对话示例和成功案例请参阅 [EXAMPLES.md](EXAMPLES.md)。 + +--- + +**Skill 版本**:v1.1 +**最后更新**:2025-10-19 +**兼容版本**:novel-writer-skills v1.0.5+ diff --git a/.agent/skills/quality-assurance/workflow-guide/SKILL.md b/.agent/skills/quality-assurance/workflow-guide/SKILL.md new file mode 100644 index 0000000..c2fb7ff --- /dev/null +++ b/.agent/skills/quality-assurance/workflow-guide/SKILL.md @@ -0,0 +1,420 @@ +--- +name: novel-writer-workflow-guide +description: "当用户开始小说项目或询问如何组织写作时使用 - 引导完成novel-writer的七步方法论并确保正确的工作流程" +allowed-tools: Read, Grep +--- + +# Novel Writer 系统化创作工作流 + +## 当用户说"我想写小说" + +**立即激活此工作流指南。** + +## 七步方法论概览 + +``` +1. /constitution → 建立创作原则 +2. /specify → 定义故事规格 +3. /clarify → 澄清模糊点 +4. /plan → 制定创作计划 +5. /tasks → 分解执行任务 +6. /write → AI 辅助写作 +7. /analyze → 质量验证分析 +``` + +## 详细步骤指导 + +### 步骤 1:建立创作原则 + +> "让我们从使用 `/constitution` 定义你的创作原则开始。这建立了你的核心价值观、质量标准和这个故事的不可协商规则。" + +**为什么这很重要**:作为你的"创作宪法" - 决策会参考这些原则。 + +**包含什么**: + +- 核心价值观(传递什么理念) +- 质量底线(绝对不妥协的) +- 风格原则(语言、节奏、氛围) +- 内容原则(角色、情节、世界观规范) + +**典型时长**:15-20 分钟 + +### 步骤 2:定义故事规格 + +> "现在使用 `/specify` 创建类似产品规格的故事文档。像产品经理一样思考:这个故事是什么,为谁写的,什么使它独特?" + +**要包含什么**: + +- 一句话概括 +- 目标读者 +- 核心冲突 +- 主要角色 +- 成功标准 + +**为什么这很重要**: + +- 明确"为谁写"和"写什么" +- 设定可衡量的成功标准 +- 为后续创作提供明确目标 + +**典型时长**:30-45 分钟 + +**特殊标记**: + +- `[需要澄清]` - 标记需要进一步明确的点 +- `[核心需求]` - 不可妥协的需求 +- `[可选特性]` - 锦上添花的内容 + +### 步骤 3:澄清关键决策 + +> "运行 `/clarify` 通过 5 个精准问题解决任何不清楚的方面。AI 会识别你的规格中的模糊点,帮助你做出清晰的决定。" + +**为什么这很重要**:规划中的模糊 = 写作中的混乱。 + +**过程**: + +- AI 识别规格中的模糊点 +- 生成最多 5 个关键问题 +- 交互式回答,答案直接更新规格 +- 保留决策历史以便回溯 + +**典型问题**: + +- "主角的核心动机是复仇还是正义?" +- "故事节奏是快速爽文还是慢热精品?" +- "结局是大团圆还是留有遗憾?" + +**典型时长**:10-15 分钟 + +### 步骤 4:制定创作计划 + +> "使用 `/plan` 创建章节结构和技术方法。这是你决定如何实现你的规格的地方。" + +**要设计什么**: + +- 章节分解与活跃情节线 +- 节奏和张力分布 +- 伏笔计划 +- 角色弧线映射 + +**为什么这很重要**: + +- 把抽象需求变成具体方案 +- 选择合适的写作方法和结构 +- 设计情节、人物、世界观的具体实现 + +**技术决策示例**: + +- "使用七点结构增强悬念" +- "采用多线叙事展现复杂性" +- "通过限定视角增加神秘感" + +**典型时长**:45-60 分钟 + +### 步骤 5:分解执行任务 + +> "执行 `/tasks` 生成带优先级、依赖关系和估算工作量的可操作任务列表。" + +**任务类型**: + +- 章节写作任务 +- 角色档案完善 +- 世界观设定补充 +- 修订和润色任务 + +**任务标记**: + +- `[P]` - 可并行执行 +- `[依赖:X]` - 依赖任务 X 完成 +- `[高优]` - 高优先级任务 + +**为什么这很重要**: + +- 大目标变成小步骤,降低创作难度 +- 明确优先级和依赖关系 +- 支持并行创作提高效率 + +**典型时长**:20-30 分钟 + +### 步骤 6:开始写作 + +> "现在 `/write` 逐章使用 AI 辅助。规格和计划指导每一章,确保一致性。" + +**写作时**: + +- 类型知识 skills 自动激活 +- 一致性检查器在后台运行 +- 节奏监控在检测到问题时提醒 + +**执行原则**: + +- 严格遵循 constitution 的原则 +- 参考 plan 中的技术方案 +- 完成 tasks 中的具体任务 +- 保持与规格的一致性 + +**推荐节奏**: + +- 每次写作会话 1-2 章 +- 每 3-5 章停下来回顾 +- 保持前进动力,不要过度编辑 + +### 步骤 7:质量检查 + +> "每 5 章运行 `/analyze` 保持质量并及早发现问题。" + +**两种分析模式**: + +- **框架分析**(写作前):验证规划 +- **内容分析**(写作后):验证执行 + +**验证维度**: + +- 宪法合规性(是否违背核心原则) +- 规格满足度(是否实现了需求) +- 内容一致性(情节、时间、人物) +- 质量标准(是否达到设定标准) + +**何时运行**: + +- 完成前 3 章后(早期验证) +- 每 5 章(定期检查) +- 完成草稿后(全面审查) + +## Skills 自动激活 + +在遵循此工作流时,相关 skills 将基于上下文激活: + +**在 `/specify` 期间**: + +- 如果你提到"言情"→ Romance skill 激活 +- 如果你提到"悬疑"→ Mystery skill 激活 +- 如果你提到"奇幻"→ Fantasy skill 激活 + +**在 `/write` 期间**: + +- 写对话 → Dialogue Techniques 激活 +- 写场景 → Scene Structure 激活 +- 任何写作 → Consistency Checker 后台运行 + +**整个过程中**: + +- Workflow Guide(此 skill)保持活跃 +- 确保你不跳过关键步骤 +- 在需要时提供提醒 + +## 当用户偏离时 + +如果用户尝试跳过步骤或直接开始写作而没有计划: + +**温和提醒**: + +> "我注意到你直接跳到写作。Novel-writer 的优势在于系统性的前期计划。你想运行 `/constitution` 和 `/specify` 建立坚实基础吗?这会使写作过程更流畅并有助于保持一致性。" + +**如果用户坚持跳过**: + +> "我理解 - 你总是可以稍后创建规划文档。但是,请注意,没有 `/specify` 和 `/plan`,你需要手动跟踪一致性。" + +## 不同规模项目的调整 + +### 短篇小说(1-3 万字) + +**简化流程**: + +``` +/specify → /clarify → /write → /analyze +``` + +**重点**:明确核心冲突和转折 + +**时间**:1-2 天密集工作 + +### 中篇小说(10-20 万字) + +**标准流程**: + +``` +完整七步 +``` + +**重点**:计划和任务分解 + +**时间**:2-6 个月 + +### 长篇连载(50 万字+) + +**强化流程**: + +``` +重复循环 plan → tasks → write → analyze +``` + +**重点**:持续的一致性验证 + +**时间**:6-18 个月 + +## 常见问题处理 + +### "我已经有大纲了" + +> "太好了!让我们使用 `/specify` 将其转换为 novel-writer 的格式。这使其机器可读并启用所有追踪功能。" + +**过程**: + +1. 将现有大纲导入 +2. 用 `/specify` 格式化 +3. 用 `/clarify` 填补空白 +4. 用 `/plan` 创建详细的技术计划 + +### "这看起来设置很多" + +> "设置需要 30-60 分钟,但节省了数小时的修订。你本质上是为你的小说创建'源代码' - 修改规格,重新生成内容。没有它,更改需要全文手动重写。" + +**收益**: + +- 保持一致性自动 +- 更改传播自动 +- 质量检查系统化 +- 修订更快更容易 + +### "我可以只使用部分工作流吗?" + +> "可以,但有些命令相互依赖: +> +> - `/write` 需要 `/specify` 和 `/plan` 才能有效工作 +> - `/analyze` 需要已写的章节来评估 +> - `/track` 需要先 `/track-init` +> +> 最小可行工作流:`/constitution` → `/specify` → `/write`" + +**完全可选**: + +- `/clarify` - 如果你的规格已经清楚 +- `/tasks` - 如果你更喜欢自由形式写作 +- `/analyze` - 虽然强烈推荐 + +### "这适合我的写作风格吗?" + +**兼容的写作风格**: + +- **规划者**:会喜欢结构 +- **裤子作者**(凭感觉写):使用轻量级规格,专注于 `/write` +- **混合型**:根据需要调整 + +**调整建议**: + +- 规划者:在 `/plan` 上花更多时间 +- 裤子作者:保持 `/specify` 简短,允许发现 +- 混合型:使用完整流程但保持灵活 + +## 进度追踪 + +### 视觉进度指示器 + +当你进行工作流时,我会显示你的位置: + +``` +✅ /constitution 完成 +✅ /specify 完成 +✅ /clarify 完成 +🔄 /plan 进行中... +⏸️ /tasks 待定 +⏸️ /write 待定 +⏸️ /analyze 待定 +``` + +### 里程碑 + +**规划阶段完成**(steps 1-5): + +- 你有完整的故事蓝图 +- 准备开始写作 +- 所有主要决策已做出 + +**第一稿完成**(步骤 6): + +- 所有章节已写 +- 准备全面分析 +- 进入修订阶段 + +**质量验证通过**(步骤 7): + +- 一致性验证 +- 符合质量标准 +- 准备发布或进一步润色 + +## 与其他 Skills 的集成 + +### 自动触发 + +**当你提到类型时**: + +- "言情"→ Romance skill 提供惯例 +- "悬疑"→ Mystery skill 提供线索布置 +- "奇幻"→ Fantasy skill 提供世界构建 + +**当你遇到困难时**: + +- "角色感觉平淡"→ 建议 Character Development commands +- "场景拖沓"→ Scene Structure skill 激活 +- "对话僵硬"→ Dialogue Techniques skill 激活 + +### 协作工作流 + +**所有 Skills 都知道工作流**: + +- 他们会参考你在工作流中的位置 +- 建议基于当前阶段适当 +- 在需要时提醒完成前面的步骤 + +## 成功模式 + +### 高生产力模式 + +``` +周一:/constitution + /specify(2-3小时) +周二:/clarify + /plan(2-3小时) +周三:/tasks + 开始 /write(2-3小时) +周四-周日:/write 会话(每天1-2小时) +下周一:/analyze + 计划下一批 +``` + +### 探索性模式 + +``` +第1周:/constitution + 轻量级 /specify +第2-3周:/write 探索性章节 +第4周:基于学习更新 /specify 和 /plan +第5-8周:结构化 /write +第9周:/analyze 和修订 +``` + +### 冲刺模式(NaNoWriMo) + +``` +第1天:快速 /constitution + /specify + /plan(4小时) +第2-27天:密集 /write(每天2000字) +第28-30天:/analyze + 紧急修复 +``` + +## 你不是孤单的 + +**在整个过程中**: + +- 我在这里指导和提醒 +- Skills 自动应用专业知识 +- 一致性自动检查 +- 你专注于创作,系统处理组织 + +**你总是掌控**: + +- 跳过你不需要的步骤 +- 调整以适应你的风格 +- 随时更改计划 +- 工具服务于你,不是相反 + +--- + +**记住**:这个工作流不是枷锁 - 它是脚手架。它支持你的创作过程,使强大的东西成为可能。一旦你熟悉它,它就变成第二天性,你会想知道你以前是如何在没有它的情况下写作的。 + +**准备开始了吗?让我们从 `/constitution` 开始!** diff --git a/.agent/skills/writing-techniques/dialogue-techniques/SKILL.md b/.agent/skills/writing-techniques/dialogue-techniques/SKILL.md new file mode 100644 index 0000000..d168d04 --- /dev/null +++ b/.agent/skills/writing-techniques/dialogue-techniques/SKILL.md @@ -0,0 +1,389 @@ +--- +name: natural-dialogue-techniques +description: "在写作对话场景或用户询问角色对话时使用 - 提供自然、符合角色性格的对话技巧,展现角色并推进情节" +allowed-tools: Read +--- + +# 自然对话写作技巧 + +## 核心原则 + +### 通过语言展现角色 + +每个角色都应该有独特的声音: + +1. **用词选择** + - 受过教育的 vs 随意的 + - 正式的 vs 俚语 + - 技术术语 vs 日常语言 + - 角色背景决定词汇 + +2. **句子结构** + - 短/断断续续 vs 长/流畅 + - 完整句子 vs 片段 + - 简单结构 vs 复杂从句 + - 反映思维方式 + +3. **言语模式** + - 打断、停顿、重复 + - 口头禅和填充词 + - 独特的措辞习惯 + - 文化和地域特色 + +4. **不说什么** + - 回避的话题 + - 说谎的模式 + - 沉默的时刻 + - 未说出口的含义 + +## 潜台词胜过直白 + +角色的意思 vs 说的话: + +### ❌ 直白对话(说教式) + +``` +"我对你生气,因为你昨晚对我撒谎了关于你去哪里。" +``` + +### ✅ 富含潜台词的对话 + +``` +"你的商务会议开得怎么样?" +"很好。" +"我确定是这样。" +``` + +**为什么更好**: + +- 让读者参与推理 +- 创造张力和不适 +- 更真实(人们避免直接冲突) +- 展示角色动态 + +### 潜台词的层次 + +**表面层**:字面意义 +**情感层**:真实感受 +**关系层**:权力和亲密度 +**主题层**:更大的故事意义 + +## 打断和重叠 + +真实对话不是有序的: + +### 使用打断 + +``` +"听着,我知道你认为——" +"你不知道我在想什么。" +"——但如果你让我说完——" +砰的一声,远处的门关上了。 +"算了。" 她转身离开。 +``` + +**何时使用打断**: + +- 情绪高涨时 +- 急迫或恐慌 +- 权力斗争 +- 展示关系动态 + +### 思维打断言语 + +``` +"我只是想说——" 他的脸闪过什么。"没事。不重要。" +``` + +**效果**: + +- 展示内在冲突 +- 创造神秘感 +- 暗示隐藏信息 +- 角色自我审查 + +## 常见错误 + +### ❌ 信息倾倒 + +**问题**:角色说话只是为了传达信息给读者 + +**坏例子**: + +``` +"如你所知,鲍勃,我们从 2015 年高中时就是朋友, +当时我们都加入了篮球队,然后我们一起去了斯坦福, +在那里学习工程学..." +``` + +**好例子**: + +``` +"还记得你三年级那个压哨球吗?" +鲍勃笑了。"教练还在谈论它。" +``` + +**解决方法**: + +- 在行动中揭示信息 +- 角色只说他们会说的话 +- 使用简短的暗示而非完整历史 +- 让读者拼凑背景 + +### ❌ 每个人听起来都一样 + +**问题**:所有角色使用相同的词汇和言语模式 + +**解决方法**:为每个主要角色创建"声音表" + +**声音表示例**: + +| 角色 | 句子长度 | 词汇 | 怪癖 | 避免什么 | +| ------ | ------------ | -------------- | ---------------- | ---------- | +| 张医生 | 中长,复杂 | 正式,医学术语 | 解释过度 | 俚语 | +| 李学生 | 短,片段 | 随意,网络语言 | "就是说"、"懂吧" | 承认不知道 | +| 王老板 | 简短,命令式 | 商业,直接 | 很少浪费词 | 解释自己 | + +### ❌ 随意言语中的完美语法 + +**太正式**: + +``` +"我要去商店。你想让我为你买些什么吗?" +``` + +**自然**: + +``` +"去商店。要我带点啥吗?" +``` + +**口语化技巧**: + +- 缩略(想要 → 想,去 → 咱) +- 省略词语(我去商店 → 去商店) +- 片段句子 +- 口头填充词(嗯、呃、那个) + +### ❌ 用对话进行叙述 + +**问题**:角色说出应该是叙述的内容 + +**坏例子**: + +``` +"我站起来,走向门,打开它。是送货员。" +``` + +**这不是对话**: + +- 人们不会叙述自己的行动 +- 这是作者偷懒 +- 使用实际叙述或展示 + +## 高级技巧 + +### 对话作为行动 + +使用言语标签展示角色状态: + +``` +"随便吧。" 她嘀咕道。(被击败) +"随便吧!" 她厉声说道。(生气) +"随便吧..." 她的声音渐渐消失。(不确定) +``` + +**超越"说"**: + +- 低语、嘀咕、咆哮(音量) +- 厉声、尖叫、吼叫(强度) +- 讥讽、嘲笑、低语(语调) +- 但不要过度使用 - "说"通常就够了 + +### 沉默作为对话 + +有时不说什么很重要: + +``` +"你爱我吗?" +他看着自己的鞋子。 +``` + +**沉默的力量**: + +- 说出言语无法说出的 +- 创造紧张 +- 展示不适或痛苦 +- 让读者填补 + +### 动作打破对话 + +不要让角色成为说话的头: + +``` +"这不是我想要的。" 她把杯子推开。"不是这样的。" + +他在房间里踱步。"那你想要什么?" + +"我——" 她的手握紧了桌边。"我不知道。" +``` + +**效果**: + +- 打破单调 +- 展示身体语言 +- 添加视觉元素 +- 创造节奏变化 + +## 对话目的 + +### 每段对话应该至少做到以下一项: + +1. **揭示角色** + - 展示性格 + - 揭示动机 + - 显示关系 + - 表明成长 + +2. **推进情节** + - 提供重要信息 + - 做出决定 + - 创造冲突 + - 解决问题 + +3. **建立氛围** + - 设定基调 + - 创造紧张 + - 提供幽默 + - 深化情感 + +4. **展示冲突** + - 目标对立 + - 误解 + - 权力斗争 + - 隐藏的议程 + +**如果对话不做这些**:删除它 + +## 特殊场景的对话 + +### 争吵/冲突 + +**有效技巧**: + +- 短句,快速来回 +- 打断频繁 +- 言语变得更尖锐 +- 可能说出后悔的话 +- 升级然后冷静(或爆发) + +``` +"你总是这样。" +"这样怎样?" +"假装——" +"我没有假装任何事!" +"——假装你在乎!" +``` + +### 浪漫/亲密 + +**有效技巧**: + +- 柔和的语调 +- 不完整的句子(情绪) +- 身体亲近描述 +- 潜台词丰富 +- 脆弱性 + +``` +"我只是..." 他的拇指擦过她的下巴。"我不想搞砸这个。" + +"那就别搞砸。" 她对他微笑。"简单。" + +"简单。" 他笑了。"对。" +``` + +### 悬疑/紧张 + +**有效技巧**: + +- 低语或简短的话 +- 不完整的想法 +- 打断(外部威胁) +- 加载停顿 +- 说不出的恐惧 + +``` +"你听到那个了吗?" + +静默。然后:远处的脚步声。 + +"我们需要——" + +一根树枝折断。两人都僵住了。 +``` + +## 与 Novel-Writer 命令集成 + +### 在 `/specify` 时 + +- 为主要角色定义独特的声音 +- 识别关键对话场景 +- 计划主要通过对话揭示什么 + +### 在 `/plan` 期间 + +- 绘制高紧张度对话场景 +- 计划信息通过对话揭示 +- 设计角色声音弧(他们如何改变说话方式) + +### 在 `/write` 时 + +- 自动应用角色声音一致性 +- 检查信息倾倒 +- 建议潜台词机会 +- 根据角色档案验证对话 + +### 在 `/analyze` 期间 + +- 检查角色声音一致性 +- 识别直白/说教对话 +- 验证每段对话都有目的 +- 建议可以加强的领域 + +## 对话写作检查清单 + +- [ ] 每个角色都有独特的声音 +- [ ] 对话推进情节或揭示角色 +- [ ] 使用潜台词而非直白 +- [ ] 包括自然的打断和重叠 +- [ ] 随意言语听起来随意 +- [ ] 没有信息倾倒 +- [ ] 动作打破大块对话 +- [ ] 沉默在适当的地方使用 +- [ ] 言语标签多样但不分散注意力 +- [ ] 每段对话都有明确目的 + +## 修订技巧 + +**大声朗读**: + +- 听起来自然吗? +- 你会被绕口令绊倒吗? +- 节奏流畅吗? + +**掩盖法**: + +- 遮住言语标签 +- 你能从对话中分辨出是谁说的吗? +- 如果不能,角色声音不够独特 + +**目的测试**: + +- 这段对话达到什么目的? +- 没有它会失去什么? +- 可以更短、更锋利吗? + +--- + +**记住**:伟大的对话感觉轻松但经过精心设计。它揭示的比说的更多,推进故事同时保持真实,并且每个字都有目的。少即是多 - 削减到精髓。 diff --git a/.agent/skills/writing-techniques/scene-structure/SKILL.md b/.agent/skills/writing-techniques/scene-structure/SKILL.md new file mode 100644 index 0000000..3a9b420 --- /dev/null +++ b/.agent/skills/writing-techniques/scene-structure/SKILL.md @@ -0,0 +1,397 @@ +--- +name: scene-structure-techniques +description: "在构建场景或规划章节内容时使用 - 提供场景-续场框架、张力管理和引人入胜场景的节拍式结构" +allowed-tools: Read +--- + +# 场景结构写作技巧 + +## 什么是场景? + +**场景**是实时发生的冲突单元,角色追求目标并面对障碍。 + +**不是场景**:说明、背景故事、旅行、时间流逝 +**是场景**:争论、谈判、追逐、揭露 + +## 场景-续场模型 + +每个场景都应该遵循这个模式: + +``` +场景(行动) 续场(反应) +├── 目标 ├── 情绪 +├── 冲突 ├── 困境 +└── 灾难/成功 └── 决定 +``` + +## 场景结构(行动) + +### 1. 目标 + +**POV角色在这个场景想要什么?** + +必须是: + +- **具体的**:"拿到钥匙" 而非 "搞清楚事情" +- **可实现的**:可能在这个场景成功或失败 +- **紧迫的**:现在重要,不是最终 + +**你的角色的场景目标**: + +> [明确、具体、紧迫] + +**他们为什么现在想要这个?**: + +> [情境/紧迫性] + +### 2. 冲突 + +**什么阻止他们得到想要的?** + +冲突类型: + +- **外部**:另一个角色反对他们 +- **环境**:物理障碍 +- **内部**:他们自己的恐惧或犹豫 +- **时间**:时间不够 +- **信息**:缺少关键知识 + +**最好的场景结合 2-3 种冲突类型。** + +**你的场景冲突**: + +1. > [主要障碍] +2. > [次要障碍] +3. > [可选第三个] + +### 3. 灾难或成功 + +**场景如何解决?** + +**灾难**(更常见): + +- 他们未能得到想要的 +- 他们得到了,但代价可怕 +- 他们得到了更糟的东西 + +**成功**(谨慎使用): + +- 他们实现目标 +- 但揭示更大的问题 +- 或成功是空洞的 + +**你的场景解决**: + +> [灾难或成功 + 后果] + +## 续场结构(反应) + +在紧张场景之后,读者需要**续场** - 角色处理的安静时刻。 + +### 1. 情绪反应 + +**角色对刚发生的事情感觉如何?** + +展示不要说: + +- **不好**:"莎拉感到悲伤" +- **好**:"莎拉的手不停颤抖" + +**你的角色的即时情绪**: + +> [情绪的身体表现] + +### 2. 困境 + +**灾难创造了一个困境 - 没有好选择:** + +- 选项 A:安全但妥协价值观 +- 选项 B:冒险但保持正直 +- 选项 C:中间路线,但不确定 + +**你的角色的困境**: + +- 选项 A:> [安全选择] +- 选项 B:> [冒险选择] +- 选项 C:> [中间地带] + +### 3. 决定 + +**他们决定做什么?** + +这个决定成为下一个场景的目标。 + +**你的角色的决定**: + +> [他们接下来要做什么] + +**这成为下一个场景的目标**,创造无缝的场景到场景连接。 + +## 场景节奏点 + +现在让我们构建场景的实际节奏(微时刻): + +### 开场节奏 + +**我们如何进入场景?** + +- 尽可能晚地开始 +- 直接进入冲突/张力 +- 快速建立 POV 和地点 + +**糟糕的开场**:"莎拉醒来,刷牙,吃早餐..." +**好的开场**:"莎拉的手机嗡嗡响。信息来自她死去的姐姐。" + +**你的开场节奏**: + +> [用户提供] + +### 上升张力节奏 + +**冲突如何升级?** + +每个节奏应该: + +1. **提高风险** +2. **使情况复杂化** +3. **揭示角色** + +**示例升级**: + +``` +节奏 1:莎拉要求文件 → 被拒绝 +节奏 2:莎拉诉诸友谊 → 老板揭示他知道她的秘密 +节奏 3:莎拉威胁辞职 → 老板揭示他一直在保护她 +节奏 4:莎拉意识到她错了 → 现在必须在忠诚中选择 +``` + +**你的升级节奏(3-5个)**: + +1. > [第一个节奏] +2. > [第二个节奏] +3. > [第三个节奏] +4. > [可选第四个] +5. > [可选第五个] + +### 高潮节奏 + +**最高张力的时刻** + +这是: + +- 角色做出关键选择的地方 +- 真相被揭示的地方 +- 行动达到峰值强度的地方 +- 一切悬而未决的地方 + +**你的高潮节奏**: + +> [用户提供] + +### 解决节奏 + +**即时后果** + +不要在高潮结束 - 给一个节奏的余波: + +- 角色的即时反应 +- 什么改变了 +- 暗示接下来会发生什么 + +**你的解决节奏**: + +> [用户提供] + +## 张力管理 + +### 张力级别 + +场景应该在强度上有所不同: + +``` +高张力(30%) ⚡️ 行动、对抗、揭露 +中等张力(50%) 🔥 调查、计划、建立 +低张力(20%) 🌊 反思、连接、设置 +``` + +**太多高张力** = 读者疲劳 +**太多低张力** = 读者无聊 + +**这个场景的张力级别是什么?** + +> [用户选择] + +**上一个场景的张力是什么?** + +> [用户提供或我参考追踪数据] + +**基于节奏的建议**: + +> [我建议这是否是好的节奏或是否应该调整] + +## 场景检查清单 + +在你写这个场景之前,验证: + +- [ ] **明确目标**:POV 角色想要具体的东西 +- [ ] **有意义的风险**:目标对角色重要 +- [ ] **重大冲突**:真正的障碍,不容易克服 +- [ ] **上升张力**:每个节奏增加压力 +- [ ] **灾难或成功**:场景以变化结束 +- [ ] **情感真实**:角色的反应是真实的 +- [ ] **故事推进**:场景推进情节或角色弧 +- [ ] **感官细节**:设定生动,不通用 +- [ ] **对话中的潜台词**:角色不直接说所有事情 +- [ ] **这个角色独有**:只有这个角色能以这种方式体验场景 + +**准备写了吗?** 我将根据你的答案提供场景大纲。 + +## 生成的场景大纲 + +基于你的答案,这是你的场景结构: + +```markdown +## 场景:[场景名称/描述] + +**POV**:[角色名称] +**地点**:[哪里] +**时间**:[故事中的何时] +**张力级别**:[高/中/低] + +### 场景目标 + +[角色] 想要 [具体目标] 因为 [紧迫性/动机]。 + +### 冲突 + +1. [主要障碍] +2. [次要障碍] +3. [额外复杂化] + +### 场景节奏 + +**开场**:[进入场景...] + +**节奏 1 - 设置**:[角色行动/情况] +**节奏 2 - 复杂化**:[引入冲突] +**节奏 3 - 升级**:[风险提高] +**节奏 4 - 危机**:[不归路] +**节奏 5 - 高潮**:[最高张力时刻] + +**解决**:[即时后果] + +### 灾难/成功 + +[场景如何结束] → [后果] + +### 续场(如果需要) + +**情绪**:[角色的反应] +**困境**:[他们权衡的选项] +**决定**:[他们选择做什么] +→ 这导致下一个场景目标:[下一个场景目标] + +### 要包含的关键元素 + +- [ ] 感官细节:[特定的景象、声音、气味] +- [ ] 对话潜台词:[什么没有被说] +- [ ] 角色特定反应:[他们如何独特地响应] +- [ ] 主题连接:[场景如何与故事主题相关] + +### 写作笔记 + +[这个场景的任何特定指导] +``` + +## 与 Novel-Writer 命令集成 + +**保存大纲到**:`scenes/[章节号]-[场景名称].md` + +**写作时**:使用 `/write` 并参考这个大纲: + +``` +/write 第5章 - 对抗场景 + +参考:scenes/chapter-5-confrontation.md +``` + +场景大纲将指导写作,相关技能将激活: + +- 对话技巧用于对话节奏 +- 节奏监控用于张力管理 +- 一致性检查用于角色行为 + +## 高级场景类型 + +一旦你熟悉基本场景,我可以指导你通过专门的场景类型: + +- **动作场景**:管理多个同时发生的事件 +- **揭露场景**:控制信息披露 +- **亲密场景**:平衡身体和情感 +- **群体场景**:管理多个角色动态 +- **回忆场景**:整合过去与现在 + +**你想要任何这些专门场景类型的指导吗?** + +## 场景续场平衡 + +**场景(行动)vs 续场(反应)的比例**: + +- **快节奏惊悚**:80% 场景,20% 续场 +- **平衡故事**:60% 场景,40% 续场 +- **角色驱动**:50% 场景,50% 续场 + +**调整基于**: + +- 类型期望 +- 当前故事阶段 +- 最近的张力级别 +- 读者需要喘息 + +## 常见场景问题 + +### 问题:场景拖沓,没有前进 + +**诊断**: + +- 没有明确目标? +- 冲突太弱? +- 太多描述,不够行动? + +**解决**: + +- 明确目标 +- 增加障碍 +- 削减到本质 + +### 问题:场景感觉通用 + +**诊断**: + +- 可能发生在任何角色身上? +- 设定是通用的"房间"或"街道"? +- 对话可以被任何人说? + +**解决**: + +- 添加角色特定的反应 +- 使用具体、独特的设定细节 +- 应用角色声音到对话 + +### 问题:读者困惑 + +**诊断**: + +- POV 不清楚? +- 太多角色同时? +- 物理空间不清楚? + +**解决**: + +- 早期建立清晰的 POV +- 限制活跃角色到 2-4 +- 描述空间布局 + +--- + +**记住**:一个精心构建的场景是有目的的伟大故事讲述的构建块。每个场景都应该改变某些东西 - 情况、关系或角色理解。如果场景结束时一切都一样,就删除它或重写它。 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d3d874 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# Compiled class file +*.class + +# Log file +*.log +logs/ + +# Package Files +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# IDE +.idea/ +*.iws +*.iml +*.ipr +.vscode/ +.settings/ +.project +.classpath + +# Node +node_modules/ + +# OS +.DS_Store +Thumbs.db + +# Local config +*.local +application-local.yml diff --git a/app/App.uvue b/app/App.uvue new file mode 100644 index 0000000..ebff13e --- /dev/null +++ b/app/App.uvue @@ -0,0 +1,60 @@ + + + diff --git a/app/PACKAGING.md b/app/PACKAGING.md new file mode 100644 index 0000000..7a5c9d8 --- /dev/null +++ b/app/PACKAGING.md @@ -0,0 +1,176 @@ +# 模拟所APP - 打包发布指南 + +## 📋 完整流程 + +### 第一步:部署后端到服务器 + +1. **打包后端项目** +```bash +cd d:/workspace/project/com-rattan-spccloud +mvn clean package -DskipTests +``` + +2. **上传JAR包到服务器** +```bash +# 生成的JAR包位置 +target/com-rattan-spccloud-1.0.jar + +# 使用scp上传到服务器 +scp target/com-rattan-spccloud-1.0.jar root@8.155.172.147:/opt/monisuo/ +``` + +3. **在服务器上启动后端** +```bash +# SSH连接服务器 +ssh root@8.155.172.147 + +# 启动服务 +cd /opt/monisuo +nohup java -jar com-rattan-spccloud-1.0.jar --spring.profiles.active=dev > app.log 2>&1 & + +# 查看日志 +tail -f app.log +``` + +4. **确保服务器防火墙开放9010端口** +```bash +# 检查端口 +netstat -tlnp | grep 9010 + +# 如果使用firewalld +firewall-cmd --zone=public --add-port=9010/tcp --permanent +firewall-cmd --reload +``` + +--- + +### 第二步:初始化数据库 + +在服务器上执行SQL脚本: +```bash +mysql -u monisuo -pJPJ8wYicSGC8aRnk monisuo < /opt/monisuo/init.sql +``` + +或者使用Navicat等工具连接数据库执行 `sql/init.sql` + +--- + +### 第三步:打包前端APP + +#### 方式一:使用HBuilderX(推荐) + +1. **下载安装 HBuilderX Alpha版** + - 下载地址:https://www.dcloud.io/hbuilderx.html + - 选择 **Alpha版**(uni-app x需要Alpha版) + +2. **导入项目** + - 打开HBuilderX + - 文件 → 导入 → 从本地目录导入 + - 选择 `d:\workspace\project\com-rattan-spccloud\app` 目录 + +3. **配置manifest.json** + - 在HBuilderX中打开 `manifest.json` + - 填写应用信息: + - App名称:模拟所 + - App描述:虚拟货币模拟交易平台 + - 版本号:1.0.0 + +4. **运行调试(可选)** + - 连接Android手机(开启USB调试) + - 运行 → 运行到手机或模拟器 → 运行到Android App基座 + +5. **云端打包** + - 发行 → 原生App-云打包 + - 选择平台:Android + - 勾选"使用DCloud公用证书"(测试用) + - 点击"打包" + - 等待打包完成,下载APK + +#### 方式二:本地打包 + +1. **生成本地打包资源** + - 发行 → 原生App-本地打包 → 生成本地打包App资源 + +2. **使用Android Studio打包** + - 打开Android Studio + - 导入生成的项目 + - Build → Build Bundle(s) / APK(s) → Build APK(s) + +--- + +### 第四步:安装APK到手机 + +#### 方式一:直接安装 +1. 将APK文件传到手机 +2. 点击APK文件安装 +3. 允许安装未知来源应用 + +#### 方式二:通过HBuilderX安装 +1. 手机连接电脑 +2. 运行 → 运行到手机或模拟器 → 运行到Android App基座 +3. 选择已连接的设备 + +--- + +## 🔧 常见问题 + +### 1. 网络请求失败 +- 检查服务器防火墙是否开放9010端口 +- 检查API地址是否正确(`app/api/request.uts`中的BASE_URL) +- 确保手机和服务器网络连通 + +### 2. 安装失败 +- 开启手机"允许安装未知来源应用" +- 卸载旧版本后再安装新版本 + +### 3. 登录失败 +- 检查数据库是否初始化成功 +- 检查后端服务是否正常运行 +- 查看后端日志:`tail -f /opt/monisuo/app.log` + +--- + +## 📱 API地址配置 + +修改 `app/api/request.uts` 文件: + +```typescript +// 开发环境(本地测试) +const BASE_URL: string = 'http://localhost:9010' + +// 生产环境(服务器部署) +const BASE_URL: string = 'http://8.155.172.147:9010' +``` + +--- + +## 🚀 快速测试 + +如果暂时没有服务器,可以使用内网穿透工具: + +1. **使用ngrok** +```bash +ngrok http 9010 +``` + +2. **修改API地址为ngrok提供的地址** +```typescript +const BASE_URL: string = 'https://xxxx.ngrok.io' +``` + +--- + +## 📦 预置账号 + +| 类型 | 账号 | 密码 | +|-----|------|------| +| 管理员 | admin | admin123 | +| 管理员 | superadmin | admin123 | + +--- + +## 🔗 相关链接 + +- [uni-app x 文档](https://doc.dcloud.net.cn/uni-app-x/) +- [HBuilderX 下载](https://www.dcloud.io/hbuilderx.html) +- [云打包说明](https://ask.dcloud.net.cn/article/37979) diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..b306086 --- /dev/null +++ b/app/README.md @@ -0,0 +1,203 @@ +# 藤编企业移动端应用 (uni-app x) + +基于 **uni-app x** 开发的跨平台移动端应用,**原生支持 Android、iOS、鸿蒙系统**。 + +## 🚀 技术栈 + +- **uni-app x** - 下一代跨平台框架 +- **Vue 3** - 前端框架 +- **UTS** - Uni Type Script(类 TypeScript) +- **Pinia** - 状态管理 +- **Vite** - 构建工具 +- **Sass** - CSS 预处理器 + +## 📱 支持平台 + +| 平台 | 支持状态 | 打包格式 | +|------|---------|---------| +| **Android** | ✅ 原生支持 | APK | +| **iOS** | ✅ 原生支持 | IPA | +| **鸿蒙 (HarmonyOS)** | ✅ 原生支持 | HAP | +| **H5** | ✅ 支持 | - | +| **微信小程序** | ✅ 支持 | - | + +## 📁 项目结构 + +``` +app/ +├── api/ # API 接口封装 (UTS) +│ ├── index.uts # 接口统一导出 +│ ├── request.uts # 请求封装 +│ └── user.uts # 用户相关接口 +├── components/ # 公共组件 (.uvue) +├── pages/ # 页面 (.uvue) +│ ├── index/ # 首页 +│ ├── login/ # 登录页 +│ └── mine/ # 我的页面 +├── static/ # 静态资源 +│ └── tabbar/ # 底部导航图标 +├── store/ # 状态管理 (Pinia) +├── utils/ # 工具函数 +├── App.uvue # 应用入口 +├── main.uts # 入口文件 +├── manifest.json # 应用配置(含鸿蒙配置) +├── pages.json # 页面路由配置 +├── uni.scss # 全局样式变量 +├── vite.config.ts # Vite 配置 +├── index.html # H5 入口页面 +└── package.json # 依赖配置 +``` + +## 🛠️ 开发环境 + +### 环境要求 + +- **Node.js** >= 18.0 +- **HBuilderX** Alpha 版本(用于打包) +- **Android Studio**(Android 打包) +- **Xcode**(iOS 打包,仅 macOS) +- **DevEco Studio**(鸿蒙打包) + +### 安装依赖 + +```bash +cd app +npm install +``` + +### 开发运行 + +```bash +# H5 开发 +npm run dev:h5 + +# Android 开发 +npm run dev:app-android + +# iOS 开发 +npm run dev:app-ios + +# 鸿蒙开发 +npm run dev:app-harmony +``` + +### 构建打包 + +```bash +# H5 构建 +npm run build:h5 + +# Android APK 构建 +npm run build:app-android + +# iOS IPA 构建 +npm run build:app-ios + +# 鸿蒙 HAP 构建 +npm run build:app-harmony +``` + +## 📦 使用 HBuilderX 打包 + +### 1. 安装 HBuilderX Alpha + +下载地址:https://www.dcloud.io/hbuilderx.html + +> **注意:uni-app x 需要使用 Alpha 版本的 HBuilderX** + +### 2. 导入项目 + +1. 打开 HBuilderX Alpha +2. 选择「文件」->「导入」->「从本地目录导入」 +3. 选择 `app` 目录 + +### 3. 运行调试 + +- **Android**: 连接手机,选择「运行」->「运行到手机或模拟器」 +- **iOS**: 连接 iPhone,选择「运行」->「运行到手机或模拟器」 +- **鸿蒙**: 连接鸿蒙设备,选择「运行」->「运行到手机或模拟器」 + +### 4. 云端打包 + +1. 选择「发行」->「原生App-云打包」 +2. 选择打包平台(Android/iOS/鸿蒙) +3. 填写证书信息 +4. 点击「打包」 + +### 5. 本地打包 + +#### Android +1. 选择「发行」->「原生App-本地打包」->「生成本地打包App资源」 +2. 使用 Android Studio 打开生成的项目 +3. 构建 APK + +#### iOS +1. 选择「发行」->「原生App-本地打包」->「生成本地打包App资源」 +2. 使用 Xcode 打开生成的项目 +3. 构建 IPA + +#### 鸿蒙 +1. 选择「发行」->「原生App-本地打包」->「生成本地打包App资源」 +2. 使用 DevEco Studio 打开生成的项目 +3. 构建 HAP + +## 🔧 API 配置 + +修改 `api/request.uts` 中的 `BASE_URL`: + +```typescript +const BASE_URL: string = 'http://your-server:9010' +``` + +## 🆕 UTS 语法说明 + +UTS (Uni Type Script) 是 uni-app x 的开发语言,语法类似 TypeScript: + +```typescript +// 变量声明 +const name: string = '藤编企业' +const count: number = 100 + +// 函数定义 +func add(a: number, b: number): number { + return a + b +} + +// 类型定义 +type User = { + id: number + name: string +} + +// 响应式数据 +const userName = ref('') +const isLoading = ref(false) + +// 计算属性 +const fullName = computed((): string => { + return `${firstName.value} ${lastName.value}` +}) +``` + +## 📝 开发注意事项 + +1. **文件后缀**: + - 页面文件使用 `.uvue` + - 脚本文件使用 `.uts` + +2. **静态资源**:请替换 `static/` 目录下的占位图片 + +3. **鸿蒙开发**:需要安装 DevEco Studio 和鸿蒙 SDK + +4. **调试**:推荐使用真机调试,模拟器可能有性能差异 + +## 🔗 相关链接 + +- [uni-app x 官方文档](https://doc.dcloud.net.cn/uni-app-x/) +- [UTS 语法指南](https://doc.dcloud.net.cn/uni-app-x/uts/) +- [HBuilderX Alpha 下载](https://www.dcloud.io/hbuilderx.html) +- [鸿蒙开发者中心](https://developer.harmonyos.com/) + +## 📄 License + +MIT License diff --git a/app/api/asset.uts b/app/api/asset.uts new file mode 100644 index 0000000..c46f014 --- /dev/null +++ b/app/api/asset.uts @@ -0,0 +1,43 @@ +/** + * 资产API + */ +import { get, post } from './request.uts' + +/** + * 获取资产总览 + */ +export func getAssetOverview (): Promise { + return get('/api/asset/overview', null) +} + +/** + * 获取资金账户 + */ +export func getFundAccount (): Promise { + return get('/api/asset/fund', null) +} + +/** + * 获取交易账户 + */ +export func getTradeAccounts (): Promise { + return get('/api/asset/trade', null) +} + +/** + * 资金划转 + */ +export func transfer (direction: number, amount: string): Promise { + return post('/api/asset/transfer', { direction, amount } as UTSJSONObject) +} + +/** + * 获取资金流水 + */ +export func getFlows (flowType: number | null, pageNum: number, pageSize: number): Promise { + const params: UTSJSONObject = { pageNum: pageNum, pageSize: pageSize } + if (flowType !== null) { + params['flowType'] = flowType + } + return get('/api/asset/flow', params) +} diff --git a/app/api/fund.uts b/app/api/fund.uts new file mode 100644 index 0000000..95ce0f0 --- /dev/null +++ b/app/api/fund.uts @@ -0,0 +1,36 @@ +/** + * 充提API + */ +import { get, post } from './request.uts' + +/** + * 申请充值 + */ +export func deposit (amount: string, remark: string | null): Promise { + return post('/api/fund/deposit', { amount, remark } as UTSJSONObject) +} + +/** + * 申请提现 + */ +export func withdraw (amount: string, remark: string | null): Promise { + return post('/api/fund/withdraw', { amount, remark } as UTSJSONObject) +} + +/** + * 取消订单 + */ +export func cancelOrder (orderNo: string): Promise { + return post('/api/fund/cancel', { orderNo } as UTSJSONObject) +} + +/** + * 获取充提记录 + */ +export func getOrders (type: number | null, pageNum: number, pageSize: number): Promise { + const params: UTSJSONObject = { pageNum: pageNum, pageSize: pageSize } + if (type !== null) { + params['type'] = type + } + return get('/api/fund/orders', params) +} diff --git a/app/api/index.uts b/app/api/index.uts new file mode 100644 index 0000000..7c6b222 --- /dev/null +++ b/app/api/index.uts @@ -0,0 +1,9 @@ +/** + * API 统一导出 + */ +export * from './request.uts' +export * from './user.uts' +export * from './market.uts' +export * from './asset.uts' +export * from './trade.uts' +export * from './fund.uts' diff --git a/app/api/market.uts b/app/api/market.uts new file mode 100644 index 0000000..e16d6b4 --- /dev/null +++ b/app/api/market.uts @@ -0,0 +1,25 @@ +/** + * 行情API + */ +import { get } from './request.uts' + +/** + * 获取币种列表 + */ +export func getCoinList (): Promise { + return get('/api/market/list', null) +} + +/** + * 获取币种详情 + */ +export func getCoinDetail (code: string): Promise { + return get('/api/market/detail', { code } as UTSJSONObject) +} + +/** + * 搜索币种 + */ +export func searchCoins (keyword: string): Promise { + return get('/api/market/search', { keyword } as UTSJSONObject) +} diff --git a/app/api/request.uts b/app/api/request.uts new file mode 100644 index 0000000..07021bd --- /dev/null +++ b/app/api/request.uts @@ -0,0 +1,130 @@ +/** + * 网络请求封装 - 模拟所APP + */ + +// API 基础地址(生产环境服务器地址) +const BASE_URL: string = 'http://8.155.172.147:5010' + +// 请求超时时间 +const TIMEOUT: number = 30000 + +// 响应数据类型 +type ResponseData = { + code: string + msg: string + data: any +} + +// 请求配置类型 +type RequestOptions = { + url: string + method?: string + data?: UTSJSONObject | null + header?: UTSJSONObject | null + timeout?: number + loading?: boolean + loadingText?: string +} + +/** + * 请求拦截器 + */ +func requestInterceptor (config: RequestOptions): RequestOptions { + const token = uni.getStorageSync('token') as string + if (token !== null && token !== '') { + config.header = { + ...config.header, + 'Authorization': `Bearer ${token}` + } as UTSJSONObject + } + + config.header = { + 'Content-Type': 'application/json', + ...config.header + } as UTSJSONObject + + return config +} + +/** + * 响应拦截器 + */ +func responseInterceptor (response: UniRequestSuccessCallbackResult): Promise { + const statusCode: number = response.statusCode + const data = response.data as ResponseData + + if (statusCode === 200) { + if (data.code === '0000') { + return Promise.resolve(data) + } else if (data.code === '0002') { + uni.removeStorageSync('token') + uni.reLaunch({ url: '/pages/login/login' }) + return Promise.reject(new Error(data.msg || '请重新登录')) + } else { + uni.showToast({ title: data.msg || '请求失败', icon: 'none', duration: 2000 }) + return Promise.reject(new Error(data.msg)) + } + } else if (statusCode === 401) { + uni.removeStorageSync('token') + uni.reLaunch({ url: '/pages/login/login' }) + return Promise.reject(new Error('未授权')) + } else { + return Promise.reject(new Error(`网络错误: ${statusCode}`)) + } +} + +/** + * 通用请求方法 + */ +func request (options: RequestOptions): Promise { + let config: RequestOptions = { + url: options.url.startsWith('http') ? options.url : BASE_URL + options.url, + method: options.method || 'GET', + data: options.data || null, + header: options.header || null, + timeout: options.timeout || TIMEOUT + } + + config = requestInterceptor(config) + + const showLoading = options.loading !== false + if (showLoading) { + uni.showLoading({ title: options.loadingText || '加载中...', mask: true }) + } + + return new Promise((resolve, reject) => { + uni.request({ + url: config.url, + method: config.method as UniRequestMethod, + data: config.data, + header: config.header, + timeout: config.timeout, + success: (response: UniRequestSuccessCallbackResult) => { + responseInterceptor(response).then(resolve).catch(reject) + }, + fail: (error: UniRequestFailCallbackResult) => { + uni.showToast({ title: error.errMsg || '网络请求失败', icon: 'none', duration: 2000 }) + reject(new Error(error.errMsg)) + }, + complete: () => { + if (showLoading) uni.hideLoading() + } + }) + }) +} + +/** + * GET 请求 + */ +export func get (url: string, params: UTSJSONObject | null = null): Promise { + return request({ url, method: 'GET', data: params }) +} + +/** + * POST 请求 + */ +export func post (url: string, data: UTSJSONObject | null = null): Promise { + return request({ url, method: 'POST', data: data }) +} + +export const config = { BASE_URL, TIMEOUT } diff --git a/app/api/trade.uts b/app/api/trade.uts new file mode 100644 index 0000000..cdb6b42 --- /dev/null +++ b/app/api/trade.uts @@ -0,0 +1,39 @@ +/** + * 交易API + */ +import { get, post } from './request.uts' + +/** + * 买入 + */ +export func buy (coinCode: string, price: string, quantity: string): Promise { + return post('/api/trade/buy', { coinCode, price, quantity } as UTSJSONObject) +} + +/** + * 卖出 + */ +export func sell (coinCode: string, price: string, quantity: string): Promise { + return post('/api/trade/sell', { coinCode, price, quantity } as UTSJSONObject) +} + +/** + * 获取交易记录 + */ +export func getOrders (coinCode: string | null, direction: number | null, pageNum: number, pageSize: number): Promise { + const params: UTSJSONObject = { pageNum: pageNum, pageSize: pageSize } + if (coinCode !== null) { + params['coinCode'] = coinCode + } + if (direction !== null) { + params['direction'] = direction + } + return get('/api/trade/orders', params) +} + +/** + * 获取订单详情 + */ +export func getOrderDetail (orderNo: string): Promise { + return get('/api/trade/order/detail', { orderNo } as UTSJSONObject) +} diff --git a/app/api/user.uts b/app/api/user.uts new file mode 100644 index 0000000..4589b7a --- /dev/null +++ b/app/api/user.uts @@ -0,0 +1,39 @@ +/** + * 用户API + */ +import { get, post } from './request.uts' + +/** + * 用户登录 + */ +export func login (username: string, password: string): Promise { + return post('/api/user/login', { username, password } as UTSJSONObject) +} + +/** + * 用户注册 + */ +export func register (username: string, password: string): Promise { + return post('/api/user/register', { username, password } as UTSJSONObject) +} + +/** + * 获取用户信息 + */ +export func getUserInfo (): Promise { + return get('/api/user/info', null) +} + +/** + * 上传KYC资料 + */ +export func uploadKyc (idCardFront: string, idCardBack: string): Promise { + return post('/api/user/kyc', { idCardFront, idCardBack } as UTSJSONObject) +} + +/** + * 退出登录 + */ +export func logout (): Promise { + return post('/api/user/logout', null) +} diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..58581cc --- /dev/null +++ b/app/index.html @@ -0,0 +1,21 @@ + + + + + + + + + + + 藤编企业 + + + + +
+ +
+ + + diff --git a/app/main.uts b/app/main.uts new file mode 100644 index 0000000..6ead2ea --- /dev/null +++ b/app/main.uts @@ -0,0 +1,15 @@ +import { createSSRApp } from 'vue' +import App from './App.vue' +import { createPinia } from 'pinia' + +export function createApp(): UTSJSONObject { + const app = createSSRApp(App) + const pinia = createPinia() + + app.use(pinia) + + return { + app, + pinia + } +} diff --git a/app/manifest.json b/app/manifest.json new file mode 100644 index 0000000..a3dc3fb --- /dev/null +++ b/app/manifest.json @@ -0,0 +1,52 @@ +{ + "name": "模拟所", + "appid": "__UNI__MONISUO01", + "description": "虚拟货币模拟交易平台,支持Android、iOS、鸿蒙系统", + "versionName": "1.0.0", + "versionCode": "100", + "transformPx": false, + "app-plus": { + "usingComponents": true, + "nvueStyleCompiler": "uni-app", + "compilerVersion": 3, + "splashscreen": { + "alwaysShowBeforeRender": true, + "waiting": true, + "autoclose": true, + "delay": 0 + }, + "distribute": { + "android": { + "permissions": [ + "", + "", + "" + ], + "minSdkVersion": 21, + "targetSdkVersion": 34 + }, + "ios": { + "dSYMs": false + } + } + }, + "quickapp": {}, + "mp-weixin": { + "appid": "", + "setting": { "urlCheck": false }, + "usingComponents": true + }, + "h5": { + "title": "模拟所", + "router": { "mode": "hash", "base": "./" } + }, + "uni-app-x": {}, + "app-harmony": { + "minSDKVersion": 11, + "targetSDKVersion": 12, + "compileSDKVersion": 12, + "package": "com.monisuo.app", + "projectName": "MonisuoApp" + }, + "vueVersion": "3" +} diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..8b5fa9e --- /dev/null +++ b/app/package.json @@ -0,0 +1,39 @@ +{ + "name": "rattan-app", + "version": "1.0.0", + "description": "藤编企业移动端应用 - 支持Android、iOS、鸿蒙", + "main": "main.js", + "scripts": { + "dev:app": "uni -p app", + "dev:app-android": "uni -p app-android", + "dev:app-ios": "uni -p app-ios", + "dev:app-harmony": "uni -p app-harmony", + "dev:h5": "uni", + "dev:mp-weixin": "uni -p mp-weixin", + "build:app": "uni build -p app", + "build:app-android": "uni build -p app-android", + "build:app-ios": "uni build -p app-ios", + "build:app-harmony": "uni build -p app-harmony", + "build:h5": "uni build", + "build:mp-weixin": "uni build -p mp-weixin" + }, + "dependencies": { + "@dcloudio/uni-app": "3.0.0-4020920250116001", + "@dcloudio/uni-app-harmony": "3.0.0-4020920250116001", + "@dcloudio/uni-app-plus": "3.0.0-4020920250116001", + "@dcloudio/uni-components": "3.0.0-4020920250116001", + "@dcloudio/uni-h5": "3.0.0-4020920250116001", + "@dcloudio/uni-mp-weixin": "3.0.0-4020920250116001", + "vue": "^3.5.13", + "pinia": "^2.3.0" + }, + "devDependencies": { + "@dcloudio/types": "^3.4.14", + "@dcloudio/uni-automator": "3.0.0-4020920250116001", + "@dcloudio/uni-cli-shared": "3.0.0-4020920250116001", + "@dcloudio/uni-stacktracey": "3.0.0-4020920250116001", + "@dcloudio/vite-plugin-uni": "3.0.0-4020920250116001", + "sass": "^1.83.0", + "vite": "^6.0.6" + } +} diff --git a/app/pages.json b/app/pages.json new file mode 100644 index 0000000..06b202d --- /dev/null +++ b/app/pages.json @@ -0,0 +1,106 @@ +{ + "pages": [ + { + "path": "pages/index/index", + "style": { + "navigationBarTitleText": "模拟所", + "navigationBarBackgroundColor": "#1A1A2E", + "navigationBarTextStyle": "white" + } + }, + { + "path": "pages/market/market", + "style": { + "navigationBarTitleText": "行情", + "navigationBarBackgroundColor": "#1A1A2E", + "navigationBarTextStyle": "white" + } + }, + { + "path": "pages/trade/trade", + "style": { + "navigationBarTitleText": "交易", + "navigationBarBackgroundColor": "#1A1A2E", + "navigationBarTextStyle": "white" + } + }, + { + "path": "pages/asset/asset", + "style": { + "navigationBarTitleText": "资产", + "navigationBarBackgroundColor": "#1A1A2E", + "navigationBarTextStyle": "white" + } + }, + { + "path": "pages/mine/mine", + "style": { + "navigationBarTitleText": "我的", + "navigationBarBackgroundColor": "#1A1A2E", + "navigationBarTextStyle": "white" + } + }, + { + "path": "pages/login/login", + "style": { + "navigationBarTitleText": "登录", + "navigationBarBackgroundColor": "#1A1A2E", + "navigationBarTextStyle": "white", + "navigationStyle": "custom" + } + }, + { + "path": "pages/register/register", + "style": { + "navigationBarTitleText": "注册", + "navigationBarBackgroundColor": "#1A1A2E", + "navigationBarTextStyle": "white", + "navigationStyle": "custom" + } + } + ], + "globalStyle": { + "navigationBarTextStyle": "white", + "navigationBarTitleText": "模拟所", + "navigationBarBackgroundColor": "#1A1A2E", + "backgroundColor": "#1A1A2E" + }, + "tabBar": { + "color": "#666666", + "selectedColor": "#00D4AA", + "borderStyle": "black", + "backgroundColor": "#16213E", + "list": [ + { + "pagePath": "pages/index/index", + "text": "首页", + "iconPath": "static/tabbar/home.png", + "selectedIconPath": "static/tabbar/home-active.png" + }, + { + "pagePath": "pages/market/market", + "text": "行情", + "iconPath": "static/tabbar/market.png", + "selectedIconPath": "static/tabbar/market-active.png" + }, + { + "pagePath": "pages/trade/trade", + "text": "交易", + "iconPath": "static/tabbar/trade.png", + "selectedIconPath": "static/tabbar/trade-active.png" + }, + { + "pagePath": "pages/asset/asset", + "text": "资产", + "iconPath": "static/tabbar/asset.png", + "selectedIconPath": "static/tabbar/asset-active.png" + }, + { + "pagePath": "pages/mine/mine", + "text": "我的", + "iconPath": "static/tabbar/mine.png", + "selectedIconPath": "static/tabbar/mine-active.png" + } + ] + } +} diff --git a/app/pages/asset/asset.uvue b/app/pages/asset/asset.uvue new file mode 100644 index 0000000..8da72f2 --- /dev/null +++ b/app/pages/asset/asset.uvue @@ -0,0 +1,448 @@ + + + + + diff --git a/app/pages/index/index.uvue b/app/pages/index/index.uvue new file mode 100644 index 0000000..d4ec591 --- /dev/null +++ b/app/pages/index/index.uvue @@ -0,0 +1,305 @@ + + + + + diff --git a/app/pages/login/login.uvue b/app/pages/login/login.uvue new file mode 100644 index 0000000..15a1a68 --- /dev/null +++ b/app/pages/login/login.uvue @@ -0,0 +1,169 @@ + + + + + diff --git a/app/pages/market/market.uvue b/app/pages/market/market.uvue new file mode 100644 index 0000000..005ebfd --- /dev/null +++ b/app/pages/market/market.uvue @@ -0,0 +1,300 @@ + + + + + diff --git a/app/pages/mine/mine.uvue b/app/pages/mine/mine.uvue new file mode 100644 index 0000000..7281fd5 --- /dev/null +++ b/app/pages/mine/mine.uvue @@ -0,0 +1,209 @@ + + + + + diff --git a/app/pages/register/register.uvue b/app/pages/register/register.uvue new file mode 100644 index 0000000..38cebb9 --- /dev/null +++ b/app/pages/register/register.uvue @@ -0,0 +1,203 @@ + + + + + diff --git a/app/pages/trade/trade.uvue b/app/pages/trade/trade.uvue new file mode 100644 index 0000000..f4bc833 --- /dev/null +++ b/app/pages/trade/trade.uvue @@ -0,0 +1,361 @@ + + + + + diff --git a/app/static/default-avatar.png b/app/static/default-avatar.png new file mode 100644 index 0000000..53d8a50 --- /dev/null +++ b/app/static/default-avatar.png @@ -0,0 +1,2 @@ + + diff --git a/app/static/logo.png b/app/static/logo.png new file mode 100644 index 0000000..4c02d3e --- /dev/null +++ b/app/static/logo.png @@ -0,0 +1,2 @@ + + diff --git a/app/static/tabbar/asset-active.png b/app/static/tabbar/asset-active.png new file mode 100644 index 0000000..ba3ecd0 --- /dev/null +++ b/app/static/tabbar/asset-active.png @@ -0,0 +1,2 @@ + + diff --git a/app/static/tabbar/asset.png b/app/static/tabbar/asset.png new file mode 100644 index 0000000..f527a21 --- /dev/null +++ b/app/static/tabbar/asset.png @@ -0,0 +1,2 @@ + + diff --git a/app/static/tabbar/home-active.png b/app/static/tabbar/home-active.png new file mode 100644 index 0000000..ba3ecd0 --- /dev/null +++ b/app/static/tabbar/home-active.png @@ -0,0 +1,2 @@ + + diff --git a/app/static/tabbar/home.png b/app/static/tabbar/home.png new file mode 100644 index 0000000..f527a21 --- /dev/null +++ b/app/static/tabbar/home.png @@ -0,0 +1,2 @@ + + diff --git a/app/static/tabbar/market-active.png b/app/static/tabbar/market-active.png new file mode 100644 index 0000000..ba3ecd0 --- /dev/null +++ b/app/static/tabbar/market-active.png @@ -0,0 +1,2 @@ + + diff --git a/app/static/tabbar/market.png b/app/static/tabbar/market.png new file mode 100644 index 0000000..f527a21 --- /dev/null +++ b/app/static/tabbar/market.png @@ -0,0 +1,2 @@ + + diff --git a/app/static/tabbar/mine-active.png b/app/static/tabbar/mine-active.png new file mode 100644 index 0000000..2a9ae54 --- /dev/null +++ b/app/static/tabbar/mine-active.png @@ -0,0 +1,2 @@ + + diff --git a/app/static/tabbar/mine.png b/app/static/tabbar/mine.png new file mode 100644 index 0000000..2e9387d --- /dev/null +++ b/app/static/tabbar/mine.png @@ -0,0 +1,2 @@ + + diff --git a/app/static/tabbar/trade-active.png b/app/static/tabbar/trade-active.png new file mode 100644 index 0000000..ba3ecd0 --- /dev/null +++ b/app/static/tabbar/trade-active.png @@ -0,0 +1,2 @@ + + diff --git a/app/static/tabbar/trade.png b/app/static/tabbar/trade.png new file mode 100644 index 0000000..f527a21 --- /dev/null +++ b/app/static/tabbar/trade.png @@ -0,0 +1,2 @@ + + diff --git a/app/uni.scss b/app/uni.scss new file mode 100644 index 0000000..57d958e --- /dev/null +++ b/app/uni.scss @@ -0,0 +1,54 @@ +/** + * uni-app 全局样式变量 - 模拟所APP + */ + +/* 主题色 */ +$primary-color: #00D4AA; +$primary-color-light: #00E6B8; +$primary-color-dark: #00B894; + +/* 状态色 */ +$success-color: #00C853; +$warning-color: #FF9800; +$error-color: #FF5252; +$info-color: #2196F3; + +/* 深色主题 */ +$bg-color-dark: #1A1A2E; +$bg-color-card: #16213E; +$text-color: #FFFFFF; +$text-color-secondary: rgba(255, 255, 255, 0.6); +$text-color-placeholder: rgba(255, 255, 255, 0.3); +$border-color: rgba(255, 255, 255, 0.1); + +/* 涨跌色 */ +$up-color: #00C853; +$down-color: #FF5252; + +/* 字体大小 */ +$font-size-xs: 22rpx; +$font-size-sm: 24rpx; +$font-size-base: 28rpx; +$font-size-md: 30rpx; +$font-size-lg: 32rpx; +$font-size-xl: 36rpx; +$font-size-xxl: 48rpx; + +/* 间距 */ +$spacing-xs: 8rpx; +$spacing-sm: 16rpx; +$spacing-base: 24rpx; +$spacing-md: 32rpx; +$spacing-lg: 48rpx; +$spacing-xl: 64rpx; + +/* 圆角 */ +$border-radius-sm: 8rpx; +$border-radius-base: 12rpx; +$border-radius-lg: 20rpx; +$border-radius-xl: 24rpx; +$border-radius-round: 999rpx; + +/* 阴影 */ +$box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.3); +$box-shadow-lg: 0 4rpx 24rpx rgba(0, 0, 0, 0.4); diff --git a/app/vite.config.ts b/app/vite.config.ts new file mode 100644 index 0000000..644529c --- /dev/null +++ b/app/vite.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from 'vite' +import uni from '@dcloudio/vite-plugin-uni' +import { resolve } from 'path' + +export default defineConfig({ + plugins: [uni()], + resolve: { + alias: { + '@': resolve(__dirname, './') + } + }, + server: { + port: 5173, + host: '0.0.0.0', + proxy: { + '/api': { + target: 'http://localhost:9010', + changeOrigin: true + } + } + }, + build: { + minify: 'terser', + terserOptions: { + compress: { + drop_console: true, + drop_debugger: true + } + } + } +}) diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0470704 --- /dev/null +++ b/pom.xml @@ -0,0 +1,145 @@ + + + 4.0.0 + +com.it.rattan +monisuo +1.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.2.4.RELEASE + + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + 2.2.4.RELEASE + + + org.springframework.boot + spring-boot-starter-web + 2.2.4.RELEASE + + + + com.alibaba + druid + 1.1.17 + + + + mysql + mysql-connector-java + runtime + + + + com.alibaba + fastjson + 1.2.47 + + + org.springframework + spring-aspects + 4.3.9.RELEASE + + + + com.ejlchina + bean-searcher-boot-starter + 3.0.1 + + + + com.baomidou + mybatis-plus-boot-starter + 3.1.2 + + + + com.baomidou + mybatis-plus-generator + 3.1.2 + + + + + + com.auth0 + java-jwt + 3.8.1 + + + + org.springframework.security + spring-security-crypto + 5.2.1.RELEASE + + + + + io.springfox + springfox-swagger2 + 2.9.2 + + + + io.springfox + springfox-swagger-ui + 2.9.2 + + + + + org.projectlombok + lombok + RELEASE + compile + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + + + src/main/java + + **/*.xml + + + + src/main/resources + + **.* + + + + + + + + + com.e-iceblue + e-iceblue + http://repo.e-iceblue.com/nexus/content/groups/public/ + + + + + diff --git a/sql/init.sql b/sql/init.sql new file mode 100644 index 0000000..71c5a99 --- /dev/null +++ b/sql/init.sql @@ -0,0 +1,257 @@ +-- ============================================= +-- 虚拟货币模拟交易平台 - 数据库初始化脚本 +-- 数据库: monisuo +-- 版本: V1.0 +-- ============================================= + +-- 设置字符集 +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- --------------------------------------------- +-- 1. 用户表 +-- --------------------------------------------- +DROP TABLE IF EXISTS `sys_user`; +CREATE TABLE `sys_user` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `username` varchar(50) NOT NULL COMMENT '账号', + `password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密)', + `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', + `avatar` varchar(255) DEFAULT NULL COMMENT '头像URL', + `phone` varchar(20) DEFAULT NULL COMMENT '手机号', + `email` varchar(100) DEFAULT NULL COMMENT '邮箱', + `kyc_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'KYC状态: 0-未激活 1-已激活', + `id_card_front` varchar(255) DEFAULT NULL COMMENT '身份证正面照URL', + `id_card_back` varchar(255) DEFAULT NULL COMMENT '身份证反面照URL', + `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态: 0-禁用 1-正常', + `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间', + `last_login_ip` varchar(50) DEFAULT NULL COMMENT '最后登录IP', + `token` varchar(500) DEFAULT NULL COMMENT '当前Token', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_username` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; + +-- --------------------------------------------- +-- 2. 管理员表 +-- --------------------------------------------- +DROP TABLE IF EXISTS `sys_admin`; +CREATE TABLE `sys_admin` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `username` varchar(50) NOT NULL COMMENT '账号', + `password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密)', + `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', + `avatar` varchar(255) DEFAULT NULL COMMENT '头像URL', + `role` tinyint(1) NOT NULL DEFAULT '2' COMMENT '角色: 1-超级管理员 2-普通管理员', + `permissions` varchar(500) DEFAULT NULL COMMENT '权限列表(JSON格式)', + `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态: 0-禁用 1-正常', + `is_system` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否系统预置: 0-否 1-是', + `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间', + `last_login_ip` varchar(50) DEFAULT NULL COMMENT '最后登录IP', + `token` varchar(500) DEFAULT NULL COMMENT '当前Token', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_username` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员表'; + +-- 初始化超级管理员账号 (密码: admin123) +INSERT INTO `sys_admin` (`username`, `password`, `nickname`, `role`, `permissions`, `is_system`) +VALUES +('admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9vz.a', '超级管理员1', 1, 'all', 1), +('superadmin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9vz.a', '超级管理员2', 1, 'all', 1); + +-- --------------------------------------------- +-- 3. 币种表 +-- --------------------------------------------- +DROP TABLE IF EXISTS `coin`; +CREATE TABLE `coin` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `code` varchar(20) NOT NULL COMMENT '币种代码(如BTC)', + `name` varchar(50) NOT NULL COMMENT '币种名称(如Bitcoin)', + `icon` varchar(255) DEFAULT NULL COMMENT '币种图标URL', + `price` decimal(20,8) NOT NULL DEFAULT '0.00000000' COMMENT '当前价格(USDT)', + `price_usd` decimal(20,8) DEFAULT NULL COMMENT '美元价格', + `price_cny` decimal(20,8) DEFAULT NULL COMMENT '人民币价格', + `price_type` tinyint(1) NOT NULL DEFAULT '2' COMMENT '价格类型: 1-实时 2-管理', + `change_24h` decimal(10,4) DEFAULT '0.0000' COMMENT '24小时涨跌幅(%)', + `high_24h` decimal(20,8) DEFAULT NULL COMMENT '24小时最高价', + `low_24h` decimal(20,8) DEFAULT NULL COMMENT '24小时最低价', + `volume_24h` decimal(20,4) DEFAULT NULL COMMENT '24小时交易量', + `market_cap` decimal(20,2) DEFAULT NULL COMMENT '市值', + `total_supply` decimal(20,2) DEFAULT NULL COMMENT '总发行量', + `circulating_supply` decimal(20,2) DEFAULT NULL COMMENT '流通量', + `description` text COMMENT '币种简介', + `website` varchar(255) DEFAULT NULL COMMENT '官网链接', + `price_scale` int(11) DEFAULT '8' COMMENT '价格小数位', + `quantity_scale` int(11) DEFAULT '8' COMMENT '数量小数位', + `min_quantity` decimal(20,8) DEFAULT '0.00000001' COMMENT '最小交易数量', + `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态: 0-下架 1-上架', + `sort` int(11) DEFAULT '0' COMMENT '排序权重(越大越靠前)', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_code` (`code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='币种表'; + +-- 初始化实时币种 +INSERT INTO `coin` (`code`, `name`, `icon`, `price`, `price_type`, `description`, `status`, `sort`) VALUES +('BTC', 'Bitcoin', '/static/coin/btc.png', 67000.00, 1, '比特币(Bitcoin)是一种点对点的电子现金系统,由中本聪于2008年提出。', 1, 100), +('ETH', 'Ethereum', '/static/coin/eth.png', 3400.00, 1, '以太坊(Ethereum)是一个开源的区块链平台,支持智能合约功能。', 1, 99), +('SOL', 'Solana', '/static/coin/sol.png', 170.00, 1, 'Solana是一个高性能区块链平台,旨在支持去中心化应用的大规模采用。', 1, 98); + +-- 初始化管理币种 +INSERT INTO `coin` (`code`, `name`, `icon`, `price`, `price_type`, `description`, `status`, `sort`) VALUES +('USDT', 'Tether', '/static/coin/usdt.png', 1.0000, 2, '泰达币(USDT)是一种与美元挂钩的稳定币。', 1, 97), +('DOGE', 'Dogecoin', '/static/coin/doge.png', 0.15, 2, '狗狗币(Dogecoin)是一种基于Scrypt算法的加密货币。', 1, 96), +('XRP', 'Ripple', '/static/coin/xrp.png', 0.50, 2, '瑞波币(XRP)是Ripple网络的原生加密货币。', 1, 95); + +-- --------------------------------------------- +-- 4. 资金账户表 +-- --------------------------------------------- +DROP TABLE IF EXISTS `account_fund`; +CREATE TABLE `account_fund` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `user_id` bigint(20) NOT NULL COMMENT '用户ID', + `balance` decimal(20,8) NOT NULL DEFAULT '0.00000000' COMMENT 'USDT余额', + `frozen` decimal(20,8) NOT NULL DEFAULT '0.00000000' COMMENT '冻结金额', + `total_deposit` decimal(20,8) NOT NULL DEFAULT '0.00000000' COMMENT '累计充值', + `total_withdraw` decimal(20,8) NOT NULL DEFAULT '0.00000000' COMMENT '累计提现', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='资金账户表'; + +-- --------------------------------------------- +-- 5. 交易账户表 +-- --------------------------------------------- +DROP TABLE IF EXISTS `account_trade`; +CREATE TABLE `account_trade` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `user_id` bigint(20) NOT NULL COMMENT '用户ID', + `coin_code` varchar(20) NOT NULL COMMENT '币种代码', + `quantity` decimal(20,8) NOT NULL DEFAULT '0.00000000' COMMENT '持仓数量', + `frozen` decimal(20,8) NOT NULL DEFAULT '0.00000000' COMMENT '冻结数量', + `avg_price` decimal(20,8) DEFAULT '0.00000000' COMMENT '平均成本价', + `total_buy` decimal(20,8) DEFAULT '0.00000000' COMMENT '累计买入数量', + `total_sell` decimal(20,8) DEFAULT '0.00000000' COMMENT '累计卖出数量', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_coin` (`user_id`, `coin_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易账户表'; + +-- --------------------------------------------- +-- 6. 交易订单表 +-- --------------------------------------------- +DROP TABLE IF EXISTS `order_trade`; +CREATE TABLE `order_trade` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `order_no` varchar(32) NOT NULL COMMENT '订单号', + `user_id` bigint(20) NOT NULL COMMENT '用户ID', + `coin_code` varchar(20) NOT NULL COMMENT '交易币种代码', + `direction` tinyint(1) NOT NULL COMMENT '交易方向: 1-买入 2-卖出', + `order_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '订单类型: 1-市价 2-限价', + `price` decimal(20,8) NOT NULL COMMENT '成交价格', + `quantity` decimal(20,8) NOT NULL COMMENT '成交数量', + `amount` decimal(20,8) NOT NULL COMMENT '成交金额(USDT)', + `fee` decimal(20,8) DEFAULT '0.00000000' COMMENT '手续费', + `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态: 1-成功 2-失败 3-已取消', + `remark` varchar(255) DEFAULT NULL COMMENT '备注', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_order_no` (`order_no`), + KEY `idx_user_id` (`user_id`), + KEY `idx_coin_code` (`coin_code`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易订单表'; + +-- --------------------------------------------- +-- 7. 充提订单表 +-- --------------------------------------------- +DROP TABLE IF EXISTS `order_fund`; +CREATE TABLE `order_fund` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `order_no` varchar(32) NOT NULL COMMENT '订单号', + `user_id` bigint(20) NOT NULL COMMENT '用户ID', + `username` varchar(50) NOT NULL COMMENT '用户账号(冗余)', + `type` tinyint(1) NOT NULL COMMENT '类型: 1-充值 2-提现', + `amount` decimal(20,8) NOT NULL COMMENT '金额(USDT)', + `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态: 1-待审批 2-已完成 3-已驳回 4-已取消', + `approve_admin_id` bigint(20) DEFAULT NULL COMMENT '审批管理员ID', + `approve_admin_name` varchar(50) DEFAULT NULL COMMENT '审批管理员名称', + `approve_time` datetime DEFAULT NULL COMMENT '审批时间', + `reject_reason` varchar(255) DEFAULT NULL COMMENT '驳回原因', + `remark` varchar(255) DEFAULT NULL COMMENT '用户备注', + `admin_remark` varchar(255) DEFAULT NULL COMMENT '管理员备注', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_order_no` (`order_no`), + KEY `idx_user_id` (`user_id`), + KEY `idx_status` (`status`), + KEY `idx_type` (`type`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='充提订单表'; + +-- --------------------------------------------- +-- 8. 资金流水表 +-- --------------------------------------------- +DROP TABLE IF EXISTS `account_flow`; +CREATE TABLE `account_flow` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `user_id` bigint(20) NOT NULL COMMENT '用户ID', + `flow_no` varchar(32) NOT NULL COMMENT '流水号', + `flow_type` tinyint(1) NOT NULL COMMENT '流水类型: 1-充值 2-提现 3-划转转入 4-划转转出 5-买入 6-卖出', + `amount` decimal(20,8) NOT NULL COMMENT '变动金额', + `balance_before` decimal(20,8) NOT NULL COMMENT '变动前余额', + `balance_after` decimal(20,8) NOT NULL COMMENT '变动后余额', + `coin_code` varchar(20) DEFAULT 'USDT' COMMENT '相关币种', + `related_order_no` varchar(32) DEFAULT NULL COMMENT '关联订单号', + `remark` varchar(255) DEFAULT NULL COMMENT '备注', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_flow_type` (`flow_type`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='资金流水表'; + +-- --------------------------------------------- +-- 9. 系统配置表 +-- --------------------------------------------- +DROP TABLE IF EXISTS `sys_config`; +CREATE TABLE `sys_config` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `config_key` varchar(100) NOT NULL COMMENT '配置键', + `config_value` varchar(500) NOT NULL COMMENT '配置值', + `config_desc` varchar(255) DEFAULT NULL COMMENT '配置说明', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_config_key` (`config_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统配置表'; + +-- 初始化系统配置 +INSERT INTO `sys_config` (`config_key`, `config_value`, `config_desc`) VALUES +('withdraw_min', '10', '最小提现金额'), +('withdraw_max', '100000', '最大单笔提现金额'), +('withdraw_daily_max', '500000', '每日最大提现金额'), +('kyc_required', 'true', '交易前是否需要KYC'); + +-- --------------------------------------------- +-- 10. 自选币种表 +-- --------------------------------------------- +DROP TABLE IF EXISTS `user_favorite`; +CREATE TABLE `user_favorite` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `user_id` bigint(20) NOT NULL COMMENT '用户ID', + `coin_code` varchar(20) NOT NULL COMMENT '币种代码', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_coin` (`user_id`, `coin_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户自选币种表'; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/src/main/java/com/it/rattan/SpcCloudApplication.java b/src/main/java/com/it/rattan/SpcCloudApplication.java new file mode 100644 index 0000000..7611e33 --- /dev/null +++ b/src/main/java/com/it/rattan/SpcCloudApplication.java @@ -0,0 +1,23 @@ +package com.it.rattan; + + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import org.springframework.boot.web.servlet.ServletComponentScan; +import org.springframework.context.annotation.ComponentScan; + + +@SpringBootApplication +@ServletComponentScan(basePackages ={"com.it.rattan"}) +@ComponentScan(basePackages ={"com.it.rattan"}) +/*@EnableAsync +@EnableAspectJAutoProxy*/ +public class SpcCloudApplication { + + public static void main(String[] args) { + SpringApplication.run(SpcCloudApplication.class, args); + } + +} diff --git a/src/main/java/com/it/rattan/SwaggerConfig.java b/src/main/java/com/it/rattan/SwaggerConfig.java new file mode 100644 index 0000000..f59743b --- /dev/null +++ b/src/main/java/com/it/rattan/SwaggerConfig.java @@ -0,0 +1,62 @@ +package com.it.rattan; + + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.ParameterBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.schema.ModelRef; +import springfox.documentation.service.*; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.contexts.SecurityContext; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.ArrayList; +import java.util.List; + +@Configuration +@EnableSwagger2 +public class SwaggerConfig { + + @Bean + public Docket api() { + return new Docket(DocumentationType.SWAGGER_2). + useDefaultResponseMessages(false) + .select() + .apis(RequestHandlerSelectors.any()) + .paths(PathSelectors.regex("^(?!auth).*$")) + .build() + .securitySchemes(securitySchemes()) + .securityContexts(securityContexts()) + ; + } + private List securitySchemes() { + List apiKey = new ArrayList(); + apiKey.add(new ApiKey("Authorization", "Authorization", "header")); + return apiKey; + } + private List securityContexts() { + List securityContexts = new ArrayList(); + SecurityContext securityContext = SecurityContext.builder() + .securityReferences(defaultAuth()) + .forPaths(PathSelectors.regex("^(?!auth).*$")) + .build(); + securityContexts.add(securityContext); + return securityContexts; + } + List defaultAuth() { + AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); + AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; + authorizationScopes[0] = authorizationScope; + List securityReferences = new ArrayList(); + SecurityReference securityContext = new SecurityReference("Authorization", authorizationScopes); + securityReferences.add(securityContext); + return securityReferences; + } +} + + + diff --git a/src/main/java/com/it/rattan/config/RattanClientConfig.java b/src/main/java/com/it/rattan/config/RattanClientConfig.java new file mode 100644 index 0000000..0bb8e0f --- /dev/null +++ b/src/main/java/com/it/rattan/config/RattanClientConfig.java @@ -0,0 +1,13 @@ +/* +package com.it.rattan.config; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Connector; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.springframework.boot.autoconfigure.SpringBootApplication; +public class RattanClientConfig { + + +} +*/ diff --git a/src/main/java/com/it/rattan/config/WebConfig.java b/src/main/java/com/it/rattan/config/WebConfig.java new file mode 100644 index 0000000..e6ed198 --- /dev/null +++ b/src/main/java/com/it/rattan/config/WebConfig.java @@ -0,0 +1,40 @@ +package com.it.rattan.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + + } + + /** + * 跨域配置 + */ + @Bean + public CorsFilter corsFilter() { + CorsConfiguration config = new CorsConfiguration(); + // 允许所有来源 + config.addAllowedOrigin("*"); + // 允许所有请求头 + config.addAllowedHeader("*"); + // 允许所有请求方法 + config.addAllowedMethod("*"); + // 允许携带凭证 + config.setAllowCredentials(true); + // 预检请求缓存时间 + config.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } +} diff --git a/src/main/java/com/it/rattan/enums/RattanMark.java b/src/main/java/com/it/rattan/enums/RattanMark.java new file mode 100644 index 0000000..26d1cb1 --- /dev/null +++ b/src/main/java/com/it/rattan/enums/RattanMark.java @@ -0,0 +1,9 @@ +package com.it.rattan.enums; + +public interface RattanMark { + + String EMPTY = ""; + + String NULL = null; + +} diff --git a/src/main/java/com/it/rattan/exception/GlobExceptionHandler.java b/src/main/java/com/it/rattan/exception/GlobExceptionHandler.java new file mode 100644 index 0000000..e856fb4 --- /dev/null +++ b/src/main/java/com/it/rattan/exception/GlobExceptionHandler.java @@ -0,0 +1,17 @@ +package com.it.rattan.exception; + +import com.it.rattan.rpc.RattanResponse; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +@ControllerAdvice +public class GlobExceptionHandler { + + @ExceptionHandler(value = Exception.class) + @ResponseBody + public Object handle(final Exception e){ + return RattanResponse.fail(e.getMessage()); + } + +} diff --git a/src/main/java/com/it/rattan/exception/LoginException.java b/src/main/java/com/it/rattan/exception/LoginException.java new file mode 100644 index 0000000..60489f9 --- /dev/null +++ b/src/main/java/com/it/rattan/exception/LoginException.java @@ -0,0 +1,21 @@ +package com.it.rattan.exception; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LoginException extends RuntimeException { + + private static final String CODE = "401"; + + private String msg; + + private String code; + + public LoginException(String msg){ + super(msg); + this.code = CODE; + } + +} diff --git a/src/main/java/com/it/rattan/intceptor/MySqlInterceptor.java b/src/main/java/com/it/rattan/intceptor/MySqlInterceptor.java new file mode 100644 index 0000000..3378c83 --- /dev/null +++ b/src/main/java/com/it/rattan/intceptor/MySqlInterceptor.java @@ -0,0 +1,103 @@ +package com.it.rattan.intceptor; + +import com.ejlchina.searcher.SearchSql; +import com.ejlchina.searcher.SqlInterceptor; +import com.ejlchina.searcher.util.StringUtils; +import com.it.rattan.SpcCloudApplication; +import org.springframework.boot.SpringApplication; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Component +public class MySqlInterceptor implements SqlInterceptor { + @Override + public SearchSql intercept(SearchSql searchSql, Map paraMap) { + if (searchSql.isShouldQueryList()) { + String listSql = searchSql.getListSqlString(); + String countSql = searchSql.getClusterSqlString(); + String prodSearchKey = (String) paraMap.get("prodName-prodModel"); + String orderByStr = ""; + if(listSql.toUpperCase().contains(" ORDER BY ")){ + orderByStr = listSql.substring(listSql.toUpperCase().indexOf("ORDER BY")); + orderByStr = orderByStr.substring(0,orderByStr.indexOf("limit")); + } + if(StringUtils.isNotBlank(prodSearchKey)){ + listSql = listSql.replace(orderByStr,""); + if(listSql.toUpperCase().contains(" WHERE ")){ + listSql = listSql.substring(0,listSql.indexOf("limit")) + "and (t.prod_name like '%"+prodSearchKey+"%' or pd.prod_Model like '%"+prodSearchKey+"%') " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit") ); + countSql = countSql + "and (t.prod_name like '%"+prodSearchKey+"%' or pd.prod_Model like '%"+prodSearchKey+"%')"; + }else{ + listSql = listSql.substring(0,listSql.indexOf("limit")) + "where (t.prod_name like '%"+prodSearchKey+"%' or pd.prod_Model like '%"+prodSearchKey+"%') " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit")); + countSql = countSql + " where (t.prod_name like '%"+prodSearchKey+"%' or pd.prod_Model like '%"+prodSearchKey+"%')"; + } + } + + String quotationSearchKey = (String) paraMap.get("projName-quotationNum"); + + if(StringUtils.isNotBlank(quotationSearchKey)){ + listSql = listSql.replace(orderByStr,""); + if(listSql.toUpperCase().contains(" WHERE ")){ + listSql = listSql.substring(0,listSql.indexOf("limit")) + "and (t.proj_Name like '%"+quotationSearchKey+"%' or t.quotation_Num like '%"+quotationSearchKey+"%') " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit") ); + countSql = countSql + "and (t.proj_Name like '%"+quotationSearchKey+"%' or t.quotation_Num like '%"+quotationSearchKey+"%')"; + }else{ + listSql = listSql.substring(0,listSql.indexOf("limit")) + "where (t.proj_Name like '%"+quotationSearchKey+"%' or t.quotation_Num like '%"+quotationSearchKey+"%') " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit")); + countSql = countSql + " where (t.proj_Name like '%"+quotationSearchKey+"%' or t.quotation_Num like '%"+quotationSearchKey+"%')"; + } + } + + String contractSearchKey = (String) paraMap.get("contractNum-contractName"); + + if(StringUtils.isNotBlank(contractSearchKey)){ + listSql = listSql.replace(orderByStr,""); + if(listSql.toUpperCase().contains(" WHERE ")){ + listSql = listSql.substring(0,listSql.indexOf("limit")) + "and (t.contract_Num like '%"+contractSearchKey+"%' or t.contract_Name like '%"+contractSearchKey+"%') " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit") ); + countSql = countSql + "and (t.contract_Num like '%"+contractSearchKey+"%' or t.contract_Name like '%"+contractSearchKey+"%')"; + }else{ + listSql = listSql.substring(0,listSql.indexOf("limit")) + "where (t.contract_Num like '%"+contractSearchKey+"%' or t.contract_Name like '%"+contractSearchKey+"%') " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit")); + countSql = countSql + " where (t.contract_Num like '%"+contractSearchKey+"%' or t.contract_Name like '%"+contractSearchKey+"%')"; + } + } + + + + String userSearchKey = (String) paraMap.get("userAccount-userName"); + if(StringUtils.isNotBlank(userSearchKey)){ + listSql = listSql.replace(orderByStr,""); + if(listSql.toUpperCase().contains(" WHERE ")){ + listSql = listSql.substring(0,listSql.indexOf("limit")) + "and (u.user_Account like '%"+userSearchKey+"%' or u.user_Name like '%"+userSearchKey+"%') " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit") ); + countSql = countSql + "and (u.user_Account like '%"+userSearchKey+"%' or u.user_Name like '%"+userSearchKey+"%')"; + }else{ + listSql = listSql.substring(0,listSql.indexOf("limit")) + "where (u.user_Account like '%"+userSearchKey+"%' or u.user_Name like '%"+userSearchKey+"%') " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit")); + countSql = countSql + " where (u.user_Account like '%"+userSearchKey+"%' or u.user_Name like '%"+userSearchKey+"%')"; + } + } + + + + String searchDay = (String) paraMap.get("searchDay"); + //YEARWEEK( FROM_UNIXTIME( `created_at`, "%Y-%m-%d %H:%i:%s" ) ,1) = YEARWEEK( now(),1 ) + if(StringUtils.isNotBlank(searchDay) && ("today".equals(searchDay) || "week".equals(searchDay))){ + listSql = listSql.replace(orderByStr,""); + if(listSql.toUpperCase().contains(" WHERE ")){ + listSql = listSql.substring(0,listSql.indexOf("limit")) + "and ("+("today".equals(searchDay) ? "to_days(t.create_time) = to_days(now()) " : "YEARWEEK(date_format(t.create_time,'%Y-%m-%d'),1)=YEARWEEK(now(),1)")+") " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit") ); + countSql = countSql + "and ("+("today".equals(searchDay) ? "to_days(t.create_time) = to_days(now()) " : "YEARWEEK(date_format(t.create_time,'%Y-%m-%d'),1)=YEARWEEK(now(),1)")+")"; + }else{ + listSql = listSql.substring(0,listSql.indexOf("limit")) + "where ("+("today".equals(searchDay) ? "to_days(t.create_time) = to_days(now()) " : "YEARWEEK(date_format(t.create_time,'%Y-%m-%d'),1)=YEARWEEK(now(),1)")+") " +(StringUtils.isNotBlank(orderByStr) ? orderByStr : "")+" "+ listSql.substring(listSql.indexOf("limit")); + countSql = countSql + " where ("+("today".equals(searchDay) ? "to_days(t.create_time) = to_days(now()) " : "YEARWEEK(date_format(t.create_time,'%Y-%m-%d'),1)=YEARWEEK(now(),1)")+")"; + } + } + searchSql.setListSqlString(listSql); + searchSql.setClusterSqlString(countSql); + } + return searchSql; + } + + + public static void main(String[] args) { + + String aa = "adasdhask order by as desc limt 5,5"; + String bb = aa.substring(aa.indexOf("order by")); + System.out.println(bb); + } +} diff --git a/src/main/java/com/it/rattan/monisuo/common/PageResult.java b/src/main/java/com/it/rattan/monisuo/common/PageResult.java new file mode 100644 index 0000000..9b98afa --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/common/PageResult.java @@ -0,0 +1,40 @@ +package com.it.rattan.monisuo.common; + +import lombok.Data; +import java.io.Serializable; +import java.util.List; + +/** + * 分页结果 + */ +@Data +public class PageResult implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 数据列表 */ + private List list; + /** 总数 */ + private long total; + /** 当前页 */ + private long pageNum; + /** 每页数量 */ + private long pageSize; + /** 总页数 */ + private long pages; + + public PageResult() { + } + + public PageResult(List list, long total, long pageNum, long pageSize) { + this.list = list; + this.total = total; + this.pageNum = pageNum; + this.pageSize = pageSize; + this.pages = (total + pageSize - 1) / pageSize; + } + + public static PageResult of(List list, long total, long pageNum, long pageSize) { + return new PageResult<>(list, total, pageNum, pageSize); + } +} diff --git a/src/main/java/com/it/rattan/monisuo/common/Result.java b/src/main/java/com/it/rattan/monisuo/common/Result.java new file mode 100644 index 0000000..62d26d5 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/common/Result.java @@ -0,0 +1,64 @@ +package com.it.rattan.monisuo.common; + +import lombok.Data; +import java.io.Serializable; + +/** + * 统一响应结果 + */ +@Data +public class Result implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 成功状态码 */ + public static final String SUCCESS_CODE = "0000"; + /** 失败状态码 */ + public static final String FAIL_CODE = "0001"; + /** 未授权状态码 */ + public static final String UNAUTHORIZED_CODE = "0002"; + + /** 状态码 */ + private String code; + /** 消息 */ + private String msg; + /** 数据 */ + private T data; + + public Result() { + } + + public Result(String code, String msg, T data) { + this.code = code; + this.msg = msg; + this.data = data; + } + + public static Result success() { + return new Result<>(SUCCESS_CODE, "操作成功", null); + } + + public static Result success(T data) { + return new Result<>(SUCCESS_CODE, "操作成功", data); + } + + public static Result success(String msg, T data) { + return new Result<>(SUCCESS_CODE, msg, data); + } + + public static Result fail(String msg) { + return new Result<>(FAIL_CODE, msg, null); + } + + public static Result fail(String code, String msg) { + return new Result<>(code, msg, null); + } + + public static Result unauthorized(String msg) { + return new Result<>(UNAUTHORIZED_CODE, msg, null); + } + + public boolean isSuccess() { + return SUCCESS_CODE.equals(this.code); + } +} diff --git a/src/main/java/com/it/rattan/monisuo/context/UserContext.java b/src/main/java/com/it/rattan/monisuo/context/UserContext.java new file mode 100644 index 0000000..7672806 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/context/UserContext.java @@ -0,0 +1,46 @@ +package com.it.rattan.monisuo.context; + +import lombok.Data; + +/** + * 用户上下文信息 + */ +@Data +public class UserContext { + + /** 用户ID */ + private Long userId; + /** 用户名 */ + private String username; + /** 类型: user/admin */ + private String type; + + private static final ThreadLocal CONTEXT = new ThreadLocal<>(); + + public static void set(UserContext context) { + CONTEXT.set(context); + } + + public static UserContext get() { + return CONTEXT.get(); + } + + public static void clear() { + CONTEXT.remove(); + } + + public static Long getUserId() { + UserContext context = get(); + return context != null ? context.getUserId() : null; + } + + public static String getUsername() { + UserContext context = get(); + return context != null ? context.getUsername() : null; + } + + public static boolean isAdmin() { + UserContext context = get(); + return context != null && "admin".equals(context.getType()); + } +} diff --git a/src/main/java/com/it/rattan/monisuo/controller/AdminController.java b/src/main/java/com/it/rattan/monisuo/controller/AdminController.java new file mode 100644 index 0000000..5aa3253 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/controller/AdminController.java @@ -0,0 +1,359 @@ +package com.it.rattan.monisuo.controller; + +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.it.rattan.monisuo.common.Result; +import com.it.rattan.monisuo.entity.*; +import com.it.rattan.monisuo.mapper.AccountFundMapper; +import com.it.rattan.monisuo.mapper.OrderFundMapper; +import com.it.rattan.monisuo.service.*; +import com.it.rattan.monisuo.util.JwtUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 管理后台接口 + */ +@RestController +@RequestMapping("/admin") +public class AdminController { + + @Autowired + private UserService userService; + + @Autowired + private CoinService coinService; + + @Autowired + private FundService fundService; + + @Autowired + private AssetService assetService; + + @Autowired + private AccountFundMapper accountFundMapper; + + @Autowired + private OrderFundMapper orderFundMapper; + + /** + * 管理员登录 + */ + @PostMapping("/login") + public Result> login(@RequestBody Map params) { + String username = params.get("username"); + String password = params.get("password"); + + if (username == null || password == null) { + return Result.fail("用户名和密码不能为空"); + } + + // 简单验证,实际应从数据库查询 + // 这里预置账号: admin/admin123, superadmin/admin123 + String validPassword = "$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt9vz.a"; // admin123 + org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder encoder = + new org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder(); + + // 临时处理:预置账号 + if (("admin".equals(username) || "superadmin".equals(username)) && + encoder.matches(password, validPassword)) { + String token = JwtUtil.createToken(1L, username, "admin"); + + Map result = new HashMap<>(); + result.put("token", token); + Map adminInfo = new HashMap<>(); + adminInfo.put("id", 1L); + adminInfo.put("username", username); + adminInfo.put("nickname", "超级管理员"); + adminInfo.put("role", 1); + result.put("adminInfo", adminInfo); + return Result.success("登录成功", result); + } + + return Result.fail("用户名或密码错误"); + } + + /** + * 用户列表 + */ + @GetMapping("/user/list") + public Result> getUserList( + @RequestParam(required = false) String username, + @RequestParam(required = false) Integer status, + @RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "20") int pageSize) { + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (username != null && !username.isEmpty()) { + wrapper.like(User::getUsername, username); + } + if (status != null) { + wrapper.eq(User::getStatus, status); + } + wrapper.orderByDesc(User::getCreateTime); + + Page page = new Page<>(pageNum, pageSize); + IPage result = userService.page(page, wrapper); + + Map data = new HashMap<>(); + data.put("list", result.getRecords()); + data.put("total", result.getTotal()); + data.put("pageNum", result.getCurrent()); + data.put("pageSize", result.getSize()); + return Result.success(data); + } + + /** + * 用户详情 + */ + @GetMapping("/user/detail") + public Result> getUserDetail(@RequestParam Long userId) { + User user = userService.getById(userId); + if (user == null) { + return Result.fail("用户不存在"); + } + + Map data = new HashMap<>(); + data.put("user", user); + + // 资产信息 + AccountFund fund = assetService.getOrCreateFundAccount(userId); + data.put("fund", fund); + + // 交易账户 + List> trade = assetService.getTradeAccount(userId); + data.put("trade", trade); + + return Result.success(data); + } + + /** + * 禁用/启用用户 + */ + @PostMapping("/user/status") + public Result updateUserStatus(@RequestBody Map params) { + Long userId = Long.valueOf(params.get("userId").toString()); + Integer status = (Integer) params.get("status"); + + User user = userService.getById(userId); + if (user == null) { + return Result.fail("用户不存在"); + } + + user.setStatus(status); + user.setUpdateTime(LocalDateTime.now()); + userService.updateById(user); + + return Result.success(status == 1 ? "已启用" : "已禁用", null); + } + + /** + * 币种列表 + */ + @GetMapping("/coin/list") + public Result> getCoinList() { + List coins = coinService.list(); + Map data = new HashMap<>(); + data.put("list", coins); + return Result.success(data); + } + + /** + * 新增/编辑币种 + */ + @PostMapping("/coin/save") + public Result saveCoin(@RequestBody Coin coin) { + if (coin.getCode() == null || coin.getCode().isEmpty()) { + return Result.fail("币种代码不能为空"); + } + if (coin.getName() == null || coin.getName().isEmpty()) { + return Result.fail("币种名称不能为空"); + } + + coin.setCode(coin.getCode().toUpperCase()); + + if (coin.getId() == null) { + // 新增 + coin.setCreateTime(LocalDateTime.now()); + coinService.save(coin); + } else { + // 编辑 + coin.setUpdateTime(LocalDateTime.now()); + coinService.updateById(coin); + } + + return Result.success("保存成功", null); + } + + /** + * 调整币种价格 + */ + @PostMapping("/coin/price") + public Result updateCoinPrice(@RequestBody Map params) { + String code = (String) params.get("code"); + BigDecimal price = new BigDecimal(params.get("price").toString()); + + Coin coin = coinService.getCoinByCode(code); + if (coin == null) { + return Result.fail("币种不存在"); + } + + if (coin.getPriceType() == 1) { + return Result.fail("实时币种价格不可手动修改"); + } + + coin.setPrice(price); + coin.setUpdateTime(LocalDateTime.now()); + coinService.updateById(coin); + + return Result.success("价格已更新", null); + } + + /** + * 币种上架/下架 + */ + @PostMapping("/coin/status") + public Result updateCoinStatus(@RequestBody Map params) { + Long coinId = Long.valueOf(params.get("coinId").toString()); + Integer status = (Integer) params.get("status"); + + Coin coin = coinService.getById(coinId); + if (coin == null) { + return Result.fail("币种不存在"); + } + + coin.setStatus(status); + coin.setUpdateTime(LocalDateTime.now()); + coinService.updateById(coin); + + return Result.success(status == 1 ? "已上架" : "已下架", null); + } + + /** + * 价格计算器 + */ + @PostMapping("/coin/calculator") + public Result> calculatePrice(@RequestBody Map params) { + BigDecimal currentPrice = new BigDecimal(params.get("currentPrice").toString()); + BigDecimal holdingAmount = new BigDecimal(params.getOrDefault("holdingAmount", "1000").toString()); + BigDecimal targetProfit = new BigDecimal(params.get("targetProfit").toString()); + + // 计算目标价格 + // 持仓数量 = 持仓金额 / 当前价格 + BigDecimal quantity = holdingAmount.divide(currentPrice, 8, BigDecimal.ROUND_DOWN); + // 单币盈亏 = 目标盈亏 / 持仓数量 + BigDecimal profitPerCoin = targetProfit.divide(quantity, 8, BigDecimal.ROUND_DOWN); + // 目标价格 = 当前价格 + 单币盈亏 + BigDecimal targetPrice = currentPrice.add(profitPerCoin); + + Map result = new HashMap<>(); + result.put("targetPrice", targetPrice); + result.put("quantity", quantity); + result.put("profitPerCoin", profitPerCoin); + + return Result.success(result); + } + + /** + * 待审批订单 + */ + @GetMapping("/order/pending") + public Result> getPendingOrders( + @RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "20") int pageSize) { + + IPage page = fundService.getPendingOrders(pageNum, pageSize); + + Map data = new HashMap<>(); + data.put("list", page.getRecords()); + data.put("total", page.getTotal()); + return Result.success(data); + } + + /** + * 所有充提订单 + */ + @GetMapping("/order/list") + public Result> getAllOrders( + @RequestParam(required = false) Integer type, + @RequestParam(required = false) Integer status, + @RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "20") int pageSize) { + + IPage page = fundService.getAllOrders(type, status, pageNum, pageSize); + + Map data = new HashMap<>(); + data.put("list", page.getRecords()); + data.put("total", page.getTotal()); + data.put("pageNum", page.getCurrent()); + data.put("pageSize", page.getSize()); + return Result.success(data); + } + + /** + * 审批订单 + */ + @PostMapping("/order/approve") + public Result approveOrder(@RequestBody Map params) { + String orderNo = (String) params.get("orderNo"); + Integer status = (Integer) params.get("status"); + String rejectReason = (String) params.get("rejectReason"); + String adminRemark = (String) params.get("adminRemark"); + + if (orderNo == null || status == null) { + return Result.fail("参数错误"); + } + + if (status != 2 && status != 3) { + return Result.fail("状态参数错误"); + } + + try { + fundService.approve(1L, "管理员", orderNo, status, rejectReason, adminRemark); + return Result.success(status == 2 ? "审批通过" : "已驳回", null); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } + + /** + * 资金总览 + */ + @GetMapping("/finance/overview") + public Result> getFinanceOverview() { + Map data = new HashMap<>(); + + // 累计充值 + BigDecimal totalDeposit = orderFundMapper.sumCompletedDeposit(); + data.put("totalDeposit", totalDeposit); + + // 累计提现 + BigDecimal totalWithdraw = orderFundMapper.sumCompletedWithdraw(); + data.put("totalWithdraw", totalWithdraw); + + // 在管资金 + BigDecimal fundBalance = accountFundMapper.sumAllBalance(); + data.put("fundBalance", fundBalance); + + // 交易账户总值 + BigDecimal tradeValue = accountFundMapper.sumAllTradeValue(); + data.put("tradeValue", tradeValue != null ? tradeValue : BigDecimal.ZERO); + + // 待审批数量 + int pendingCount = orderFundMapper.countPending(); + data.put("pendingCount", pendingCount); + + // 用户总数 + long userCount = userService.count(); + data.put("userCount", userCount); + + return Result.success(data); + } +} diff --git a/src/main/java/com/it/rattan/monisuo/controller/AssetController.java b/src/main/java/com/it/rattan/monisuo/controller/AssetController.java new file mode 100644 index 0000000..7c8a7d9 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/controller/AssetController.java @@ -0,0 +1,129 @@ +package com.it.rattan.monisuo.controller; + +import com.it.rattan.monisuo.common.Result; +import com.it.rattan.monisuo.context.UserContext; +import com.it.rattan.monisuo.service.AssetService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +/** + * 资产接口 + */ +@RestController +@RequestMapping("/api/asset") +public class AssetController { + + @Autowired + private AssetService assetService; + + /** + * 资产总览 + */ + @GetMapping("/overview") + public Result> getOverview() { + Long userId = UserContext.getUserId(); + if (userId == null) { + return Result.unauthorized("请先登录"); + } + + try { + Map result = assetService.getOverview(userId); + return Result.success(result); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } + + /** + * 资金账户 + */ + @GetMapping("/fund") + public Result> getFundAccount() { + Long userId = UserContext.getUserId(); + if (userId == null) { + return Result.unauthorized("请先登录"); + } + + try { + Map result = new java.util.HashMap<>(); + result.put("fund", assetService.getOrCreateFundAccount(userId)); + return Result.success(result); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } + + /** + * 交易账户 + */ + @GetMapping("/trade") + public Result> getTradeAccount() { + Long userId = UserContext.getUserId(); + if (userId == null) { + return Result.unauthorized("请先登录"); + } + + try { + List> positions = assetService.getTradeAccount(userId); + Map result = new java.util.HashMap<>(); + result.put("positions", positions); + return Result.success(result); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } + + /** + * 资金划转 + */ + @PostMapping("/transfer") + public Result transfer(@RequestBody Map params) { + Long userId = UserContext.getUserId(); + if (userId == null) { + return Result.unauthorized("请先登录"); + } + + Integer direction = (Integer) params.get("direction"); + BigDecimal amount = new BigDecimal(params.get("amount").toString()); + + if (direction == null || (direction != 1 && direction != 2)) { + return Result.fail("请选择划转方向"); + } + if (amount.compareTo(BigDecimal.ZERO) <= 0) { + return Result.fail("划转金额必须大于0"); + } + + try { + assetService.transfer(userId, direction, amount); + return Result.success("划转成功", null); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } + + /** + * 资金流水 + */ + @GetMapping("/flow") + public Result> getFlows( + @RequestParam(required = false) Integer flowType, + @RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "20") int pageSize) { + Long userId = UserContext.getUserId(); + if (userId == null) { + return Result.unauthorized("请先登录"); + } + + try { + List flows = assetService.getFlows(userId, flowType, pageNum, pageSize); + Map result = new java.util.HashMap<>(); + result.put("list", flows); + return Result.success(result); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } +} diff --git a/src/main/java/com/it/rattan/monisuo/controller/FundController.java b/src/main/java/com/it/rattan/monisuo/controller/FundController.java new file mode 100644 index 0000000..b80825e --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/controller/FundController.java @@ -0,0 +1,121 @@ +package com.it.rattan.monisuo.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.it.rattan.monisuo.common.Result; +import com.it.rattan.monisuo.context.UserContext; +import com.it.rattan.monisuo.entity.OrderFund; +import com.it.rattan.monisuo.service.FundService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import java.math.BigDecimal; +import java.util.Map; + +/** + * 充提接口 + */ +@RestController +@RequestMapping("/api/fund") +public class FundController { + + @Autowired + private FundService fundService; + + /** + * 申请充值 + */ + @PostMapping("/deposit") + public Result> deposit(@RequestBody Map params) { + Long userId = UserContext.getUserId(); + if (userId == null) { + return Result.unauthorized("请先登录"); + } + + BigDecimal amount = new BigDecimal(params.get("amount").toString()); + String remark = (String) params.get("remark"); + + if (amount.compareTo(BigDecimal.ZERO) <= 0) { + return Result.fail("充值金额必须大于0"); + } + + try { + Map result = fundService.deposit(userId, amount, remark); + return Result.success("申请成功,等待审批", result); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } + + /** + * 申请提现 + */ + @PostMapping("/withdraw") + public Result> withdraw(@RequestBody Map params) { + Long userId = UserContext.getUserId(); + if (userId == null) { + return Result.unauthorized("请先登录"); + } + + BigDecimal amount = new BigDecimal(params.get("amount").toString()); + String remark = (String) params.get("remark"); + + if (amount.compareTo(BigDecimal.ZERO) <= 0) { + return Result.fail("提现金额必须大于0"); + } + + try { + Map result = fundService.withdraw(userId, amount, remark); + return Result.success("申请成功,等待审批", result); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } + + /** + * 取消订单 + */ + @PostMapping("/cancel") + public Result cancel(@RequestBody Map params) { + Long userId = UserContext.getUserId(); + if (userId == null) { + return Result.unauthorized("请先登录"); + } + + String orderNo = params.get("orderNo"); + if (orderNo == null || orderNo.isEmpty()) { + return Result.fail("订单号不能为空"); + } + + try { + fundService.cancel(userId, orderNo); + return Result.success("取消成功", null); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } + + /** + * 充提记录 + */ + @GetMapping("/orders") + public Result> getOrders( + @RequestParam(required = false) Integer type, + @RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "20") int pageSize) { + Long userId = UserContext.getUserId(); + if (userId == null) { + return Result.unauthorized("请先登录"); + } + + try { + IPage page = fundService.getOrders(userId, type, pageNum, pageSize); + Map result = new java.util.HashMap<>(); + result.put("list", page.getRecords()); + result.put("total", page.getTotal()); + result.put("pageNum", page.getCurrent()); + result.put("pageSize", page.getSize()); + return Result.success(result); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } +} diff --git a/src/main/java/com/it/rattan/monisuo/controller/MarketController.java b/src/main/java/com/it/rattan/monisuo/controller/MarketController.java new file mode 100644 index 0000000..99d3a08 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/controller/MarketController.java @@ -0,0 +1,74 @@ +package com.it.rattan.monisuo.controller; + +import com.it.rattan.monisuo.common.Result; +import com.it.rattan.monisuo.entity.Coin; +import com.it.rattan.monisuo.service.CoinService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 行情接口 + */ +@RestController +@RequestMapping("/api/market") +public class MarketController { + + @Autowired + private CoinService coinService; + + /** + * 币种列表 + */ + @GetMapping("/list") + public Result> getCoinList() { + try { + List coins = coinService.getActiveCoins(); + Map result = new HashMap<>(); + result.put("list", coins); + return Result.success(result); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } + + /** + * 币种详情 + */ + @GetMapping("/detail") + public Result getCoinDetail(@RequestParam String code) { + try { + Coin coin = coinService.getCoinByCode(code); + if (coin == null) { + return Result.fail("币种不存在"); + } + return Result.success(coin); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } + + /** + * 搜索币种 + */ + @GetMapping("/search") + public Result> searchCoins(@RequestParam String keyword) { + try { + List coins = coinService.getActiveCoins(); + List filtered = new java.util.ArrayList<>(); + for (Coin coin : coins) { + if (coin.getCode().toLowerCase().contains(keyword.toLowerCase()) || + coin.getName().toLowerCase().contains(keyword.toLowerCase())) { + filtered.add(coin); + } + } + Map result = new HashMap<>(); + result.put("list", filtered); + return Result.success(result); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } +} diff --git a/src/main/java/com/it/rattan/monisuo/controller/TradeController.java b/src/main/java/com/it/rattan/monisuo/controller/TradeController.java new file mode 100644 index 0000000..bf57995 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/controller/TradeController.java @@ -0,0 +1,134 @@ +package com.it.rattan.monisuo.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.it.rattan.monisuo.common.Result; +import com.it.rattan.monisuo.context.UserContext; +import com.it.rattan.monisuo.entity.OrderTrade; +import com.it.rattan.monisuo.service.TradeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import java.math.BigDecimal; +import java.util.Map; + +/** + * 交易接口 + */ +@RestController +@RequestMapping("/api/trade") +public class TradeController { + + @Autowired + private TradeService tradeService; + + /** + * 买入 + */ + @PostMapping("/buy") + public Result> buy(@RequestBody Map params) { + Long userId = UserContext.getUserId(); + if (userId == null) { + return Result.unauthorized("请先登录"); + } + + String coinCode = (String) params.get("coinCode"); + BigDecimal price = new BigDecimal(params.get("price").toString()); + BigDecimal quantity = new BigDecimal(params.get("quantity").toString()); + + if (coinCode == null || coinCode.isEmpty()) { + return Result.fail("请选择币种"); + } + if (price.compareTo(BigDecimal.ZERO) <= 0) { + return Result.fail("价格必须大于0"); + } + if (quantity.compareTo(BigDecimal.ZERO) <= 0) { + return Result.fail("数量必须大于0"); + } + + try { + Map result = tradeService.buy(userId, coinCode.toUpperCase(), price, quantity); + return Result.success("买入成功", result); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } + + /** + * 卖出 + */ + @PostMapping("/sell") + public Result> sell(@RequestBody Map params) { + Long userId = UserContext.getUserId(); + if (userId == null) { + return Result.unauthorized("请先登录"); + } + + String coinCode = (String) params.get("coinCode"); + BigDecimal price = new BigDecimal(params.get("price").toString()); + BigDecimal quantity = new BigDecimal(params.get("quantity").toString()); + + if (coinCode == null || coinCode.isEmpty()) { + return Result.fail("请选择币种"); + } + if (price.compareTo(BigDecimal.ZERO) <= 0) { + return Result.fail("价格必须大于0"); + } + if (quantity.compareTo(BigDecimal.ZERO) <= 0) { + return Result.fail("数量必须大于0"); + } + + try { + Map result = tradeService.sell(userId, coinCode.toUpperCase(), price, quantity); + return Result.success("卖出成功", result); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } + + /** + * 交易记录 + */ + @GetMapping("/orders") + public Result> getOrders( + @RequestParam(required = false) String coinCode, + @RequestParam(required = false) Integer direction, + @RequestParam(defaultValue = "1") int pageNum, + @RequestParam(defaultValue = "20") int pageSize) { + Long userId = UserContext.getUserId(); + if (userId == null) { + return Result.unauthorized("请先登录"); + } + + try { + IPage page = tradeService.getOrders(userId, coinCode, direction, pageNum, pageSize); + Map result = new java.util.HashMap<>(); + result.put("list", page.getRecords()); + result.put("total", page.getTotal()); + result.put("pageNum", page.getCurrent()); + result.put("pageSize", page.getSize()); + return Result.success(result); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } + + /** + * 订单详情 + */ + @GetMapping("/order/detail") + public Result getOrderDetail(@RequestParam String orderNo) { + Long userId = UserContext.getUserId(); + if (userId == null) { + return Result.unauthorized("请先登录"); + } + + try { + OrderTrade order = tradeService.getOrderDetail(userId, orderNo); + if (order == null) { + return Result.fail("订单不存在"); + } + return Result.success(order); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } +} diff --git a/src/main/java/com/it/rattan/monisuo/controller/UserController.java b/src/main/java/com/it/rattan/monisuo/controller/UserController.java new file mode 100644 index 0000000..b2d8b50 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/controller/UserController.java @@ -0,0 +1,120 @@ +package com.it.rattan.monisuo.controller; + +import com.it.rattan.monisuo.common.Result; +import com.it.rattan.monisuo.context.UserContext; +import com.it.rattan.monisuo.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import java.util.Map; + +/** + * 用户接口 + */ +@RestController +@RequestMapping("/api/user") +public class UserController { + + @Autowired + private UserService userService; + + /** + * 用户注册 + */ + @PostMapping("/register") + public Result> register(@RequestBody Map params) { + String username = params.get("username"); + String password = params.get("password"); + + if (username == null || username.trim().isEmpty()) { + return Result.fail("用户名不能为空"); + } + if (password == null || password.length() < 6) { + return Result.fail("密码长度至少6位"); + } + + try { + Map result = userService.register(username.trim(), password); + return Result.success("注册成功", result); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } + + /** + * 用户登录 + */ + @PostMapping("/login") + public Result> login(@RequestBody Map params) { + String username = params.get("username"); + String password = params.get("password"); + + if (username == null || username.trim().isEmpty()) { + return Result.fail("用户名不能为空"); + } + if (password == null || password.isEmpty()) { + return Result.fail("密码不能为空"); + } + + try { + Map result = userService.login(username.trim(), password); + return Result.success("登录成功", result); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } + + /** + * 获取用户信息 + */ + @GetMapping("/info") + public Result> getUserInfo() { + Long userId = UserContext.getUserId(); + if (userId == null) { + return Result.unauthorized("请先登录"); + } + + try { + Map result = userService.getUserInfo(userId); + return Result.success(result); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } + + /** + * 上传KYC资料 + */ + @PostMapping("/kyc") + public Result uploadKyc(@RequestBody Map params) { + Long userId = UserContext.getUserId(); + if (userId == null) { + return Result.unauthorized("请先登录"); + } + + String idCardFront = params.get("idCardFront"); + String idCardBack = params.get("idCardBack"); + + if (idCardFront == null || idCardFront.isEmpty()) { + return Result.fail("请上传身份证正面照"); + } + if (idCardBack == null || idCardBack.isEmpty()) { + return Result.fail("请上传身份证反面照"); + } + + try { + userService.uploadKyc(userId, idCardFront, idCardBack); + return Result.success("上传成功", null); + } catch (Exception e) { + return Result.fail(e.getMessage()); + } + } + + /** + * 退出登录 + */ + @PostMapping("/logout") + public Result logout() { + // 客户端清除Token即可 + return Result.success("退出成功", null); + } +} diff --git a/src/main/java/com/it/rattan/monisuo/entity/AccountFlow.java b/src/main/java/com/it/rattan/monisuo/entity/AccountFlow.java new file mode 100644 index 0000000..b9f5f8f --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/entity/AccountFlow.java @@ -0,0 +1,51 @@ +package com.it.rattan.monisuo.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 资金流水实体类 + */ +@Data +@TableName("account_flow") +public class AccountFlow implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + /** 用户ID */ + private Long userId; + + /** 流水号 */ + private String flowNo; + + /** 流水类型: 1-充值 2-提现 3-划转转入 4-划转转出 5-买入 6-卖出 */ + private Integer flowType; + + /** 变动金额 */ + private BigDecimal amount; + + /** 变动前余额 */ + private BigDecimal balanceBefore; + + /** 变动后余额 */ + private BigDecimal balanceAfter; + + /** 相关币种 */ + private String coinCode; + + /** 关联订单号 */ + private String relatedOrderNo; + + /** 备注 */ + private String remark; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; +} diff --git a/src/main/java/com/it/rattan/monisuo/entity/AccountFund.java b/src/main/java/com/it/rattan/monisuo/entity/AccountFund.java new file mode 100644 index 0000000..de1e5f0 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/entity/AccountFund.java @@ -0,0 +1,43 @@ +package com.it.rattan.monisuo.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 资金账户实体类 + */ +@Data +@TableName("account_fund") +public class AccountFund implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + /** 用户ID */ + private Long userId; + + /** USDT余额 */ + private BigDecimal balance; + + /** 冻结金额 */ + private BigDecimal frozen; + + /** 累计充值 */ + private BigDecimal totalDeposit; + + /** 累计提现 */ + private BigDecimal totalWithdraw; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/it/rattan/monisuo/entity/AccountTrade.java b/src/main/java/com/it/rattan/monisuo/entity/AccountTrade.java new file mode 100644 index 0000000..0c69797 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/entity/AccountTrade.java @@ -0,0 +1,49 @@ +package com.it.rattan.monisuo.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 交易账户实体类 + */ +@Data +@TableName("account_trade") +public class AccountTrade implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + /** 用户ID */ + private Long userId; + + /** 币种代码 */ + private String coinCode; + + /** 持仓数量 */ + private BigDecimal quantity; + + /** 冻结数量 */ + private BigDecimal frozen; + + /** 平均成本价 */ + private BigDecimal avgPrice; + + /** 累计买入数量 */ + private BigDecimal totalBuy; + + /** 累计卖出数量 */ + private BigDecimal totalSell; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/it/rattan/monisuo/entity/Admin.java b/src/main/java/com/it/rattan/monisuo/entity/Admin.java new file mode 100644 index 0000000..968db6f --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/entity/Admin.java @@ -0,0 +1,61 @@ +package com.it.rattan.monisuo.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 管理员实体类 + */ +@Data +@TableName("sys_admin") +public class Admin implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + /** 账号 */ + private String username; + + /** 密码(BCrypt加密) */ + private String password; + + /** 昵称 */ + private String nickname; + + /** 头像URL */ + private String avatar; + + /** 角色: 1-超级管理员 2-普通管理员 */ + private Integer role; + + /** 权限列表(JSON格式) */ + private String permissions; + + /** 状态: 0-禁用 1-正常 */ + private Integer status; + + /** 是否系统预置: 0-否 1-是 */ + private Integer isSystem; + + /** 最后登录时间 */ + private LocalDateTime lastLoginTime; + + /** 最后登录IP */ + private String lastLoginIp; + + /** 当前Token */ + @TableField(select = false) + private String token; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/it/rattan/monisuo/entity/Coin.java b/src/main/java/com/it/rattan/monisuo/entity/Coin.java new file mode 100644 index 0000000..eb7b842 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/entity/Coin.java @@ -0,0 +1,91 @@ +package com.it.rattan.monisuo.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 币种实体类 + */ +@Data +@TableName("coin") +public class Coin implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + /** 币种代码(如BTC) */ + private String code; + + /** 币种名称(如Bitcoin) */ + private String name; + + /** 币种图标URL */ + private String icon; + + /** 当前价格(USDT) */ + private BigDecimal price; + + /** 美元价格 */ + private BigDecimal priceUsd; + + /** 人民币价格 */ + private BigDecimal priceCny; + + /** 价格类型: 1-实时 2-管理 */ + private Integer priceType; + + /** 24小时涨跌幅(%) */ + private BigDecimal change24h; + + /** 24小时最高价 */ + private BigDecimal high24h; + + /** 24小时最低价 */ + private BigDecimal low24h; + + /** 24小时交易量 */ + private BigDecimal volume24h; + + /** 市值 */ + private BigDecimal marketCap; + + /** 总发行量 */ + private BigDecimal totalSupply; + + /** 流通量 */ + private BigDecimal circulatingSupply; + + /** 币种简介 */ + private String description; + + /** 官网链接 */ + private String website; + + /** 价格小数位 */ + private Integer priceScale; + + /** 数量小数位 */ + private Integer quantityScale; + + /** 最小交易数量 */ + private BigDecimal minQuantity; + + /** 状态: 0-下架 1-上架 */ + private Integer status; + + /** 排序权重(越大越靠前) */ + private Integer sort; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/it/rattan/monisuo/entity/OrderFund.java b/src/main/java/com/it/rattan/monisuo/entity/OrderFund.java new file mode 100644 index 0000000..5e52875 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/entity/OrderFund.java @@ -0,0 +1,64 @@ +package com.it.rattan.monisuo.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 充提订单实体类 + */ +@Data +@TableName("order_fund") +public class OrderFund implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + /** 订单号 */ + private String orderNo; + + /** 用户ID */ + private Long userId; + + /** 用户账号(冗余) */ + private String username; + + /** 类型: 1-充值 2-提现 */ + private Integer type; + + /** 金额(USDT) */ + private BigDecimal amount; + + /** 状态: 1-待审批 2-已完成 3-已驳回 4-已取消 */ + private Integer status; + + /** 审批管理员ID */ + private Long approveAdminId; + + /** 审批管理员名称 */ + private String approveAdminName; + + /** 审批时间 */ + private LocalDateTime approveTime; + + /** 驳回原因 */ + private String rejectReason; + + /** 用户备注 */ + private String remark; + + /** 管理员备注 */ + private String adminRemark; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/it/rattan/monisuo/entity/OrderTrade.java b/src/main/java/com/it/rattan/monisuo/entity/OrderTrade.java new file mode 100644 index 0000000..d40cdbb --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/entity/OrderTrade.java @@ -0,0 +1,61 @@ +package com.it.rattan.monisuo.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 交易订单实体类 + */ +@Data +@TableName("order_trade") +public class OrderTrade implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + /** 订单号 */ + private String orderNo; + + /** 用户ID */ + private Long userId; + + /** 交易币种代码 */ + private String coinCode; + + /** 交易方向: 1-买入 2-卖出 */ + private Integer direction; + + /** 订单类型: 1-市价 2-限价 */ + private Integer orderType; + + /** 成交价格 */ + private BigDecimal price; + + /** 成交数量 */ + private BigDecimal quantity; + + /** 成交金额(USDT) */ + private BigDecimal amount; + + /** 手续费 */ + private BigDecimal fee; + + /** 状态: 1-成功 2-失败 3-已取消 */ + private Integer status; + + /** 备注 */ + private String remark; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/it/rattan/monisuo/entity/User.java b/src/main/java/com/it/rattan/monisuo/entity/User.java new file mode 100644 index 0000000..7194efe --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/entity/User.java @@ -0,0 +1,68 @@ +package com.it.rattan.monisuo.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 用户实体类 + */ +@Data +@TableName("sys_user") +public class User implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + /** 账号 */ + private String username; + + /** 密码(BCrypt加密) */ + private String password; + + /** 昵称 */ + private String nickname; + + /** 头像URL */ + private String avatar; + + /** 手机号 */ + private String phone; + + /** 邮箱 */ + private String email; + + /** KYC状态: 0-未激活 1-已激活 */ + private Integer kycStatus; + + /** 身份证正面照URL */ + private String idCardFront; + + /** 身份证反面照URL */ + private String idCardBack; + + /** 状态: 0-禁用 1-正常 */ + private Integer status; + + /** 最后登录时间 */ + private LocalDateTime lastLoginTime; + + /** 最后登录IP */ + private String lastLoginIp; + + /** 当前Token */ + @TableField(select = false) + private String token; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/it/rattan/monisuo/filter/TokenFilter.java b/src/main/java/com/it/rattan/monisuo/filter/TokenFilter.java new file mode 100644 index 0000000..7baee07 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/filter/TokenFilter.java @@ -0,0 +1,86 @@ +package com.it.rattan.monisuo.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.it.rattan.monisuo.common.Result; +import com.it.rattan.monisuo.context.UserContext; +import com.it.rattan.monisuo.util.JwtUtil; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Token过滤器 + */ +@Component +@Order(1) +public class TokenFilter implements Filter { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + /** 不需要验证的路径 */ + private static final String[] EXCLUDE_PATHS = { + "/api/user/register", + "/api/user/login", + "/admin/login", + "/swagger-resources", + "/v2/api-docs", + "/webjars/", + "/swagger-ui.html" + }; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + String uri = httpRequest.getRequestURI(); + + // 检查是否排除路径 + for (String excludePath : EXCLUDE_PATHS) { + if (uri.contains(excludePath)) { + chain.doFilter(request, response); + return; + } + } + + // 获取Token + String token = httpRequest.getHeader("Authorization"); + if (token != null && token.startsWith("Bearer ")) { + token = token.substring(7); + } + + if (token == null || token.isEmpty()) { + writeUnauthorized(httpResponse, "请先登录"); + return; + } + + // 验证Token + if (!JwtUtil.isValid(token)) { + writeUnauthorized(httpResponse, "Token已过期,请重新登录"); + return; + } + + // 设置用户上下文 + UserContext context = new UserContext(); + context.setUserId(JwtUtil.getUserId(token)); + context.setUsername(JwtUtil.getUsername(token)); + context.setType(JwtUtil.getType(token)); + UserContext.set(context); + + try { + chain.doFilter(request, response); + } finally { + UserContext.clear(); + } + } + + private void writeUnauthorized(HttpServletResponse response, String message) throws IOException { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().write(OBJECT_MAPPER.writeValueAsString(Result.unauthorized(message))); + } +} diff --git a/src/main/java/com/it/rattan/monisuo/mapper/AccountFlowMapper.java b/src/main/java/com/it/rattan/monisuo/mapper/AccountFlowMapper.java new file mode 100644 index 0000000..320529c --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/mapper/AccountFlowMapper.java @@ -0,0 +1,12 @@ +package com.it.rattan.monisuo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.it.rattan.monisuo.entity.AccountFlow; +import org.apache.ibatis.annotations.Mapper; + +/** + * 资金流水Mapper + */ +@Mapper +public interface AccountFlowMapper extends BaseMapper { +} diff --git a/src/main/java/com/it/rattan/monisuo/mapper/AccountFundMapper.java b/src/main/java/com/it/rattan/monisuo/mapper/AccountFundMapper.java new file mode 100644 index 0000000..89898b2 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/mapper/AccountFundMapper.java @@ -0,0 +1,26 @@ +package com.it.rattan.monisuo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.it.rattan.monisuo.entity.AccountFund; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; +import java.math.BigDecimal; + +/** + * 资金账户Mapper + */ +@Mapper +public interface AccountFundMapper extends BaseMapper { + + @Select("SELECT IFNULL(SUM(balance), 0) FROM account_fund") + BigDecimal sumAllBalance(); + + @Select("SELECT IFNULL(SUM(total_deposit), 0) FROM account_fund") + BigDecimal sumTotalDeposit(); + + @Select("SELECT IFNULL(SUM(total_withdraw), 0) FROM account_fund") + BigDecimal sumTotalWithdraw(); + + @Select("SELECT IFNULL(SUM(quantity * avg_price), 0) FROM account_trade") + BigDecimal sumAllTradeValue(); +} diff --git a/src/main/java/com/it/rattan/monisuo/mapper/AccountTradeMapper.java b/src/main/java/com/it/rattan/monisuo/mapper/AccountTradeMapper.java new file mode 100644 index 0000000..3b14ce8 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/mapper/AccountTradeMapper.java @@ -0,0 +1,19 @@ +package com.it.rattan.monisuo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.it.rattan.monisuo.entity.AccountTrade; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; +import java.math.BigDecimal; + +/** + * 交易账户Mapper + */ +@Mapper +public interface AccountTradeMapper extends BaseMapper { + + @Select("SELECT IFNULL(SUM(at.quantity * c.price), 0) FROM account_trade at " + + "LEFT JOIN coin c ON at.coin_code = c.code " + + "WHERE at.quantity > 0") + BigDecimal sumAllTradeValue(); +} diff --git a/src/main/java/com/it/rattan/monisuo/mapper/AdminMapper.java b/src/main/java/com/it/rattan/monisuo/mapper/AdminMapper.java new file mode 100644 index 0000000..0e7c833 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/mapper/AdminMapper.java @@ -0,0 +1,12 @@ +package com.it.rattan.monisuo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.it.rattan.monisuo.entity.Admin; +import org.apache.ibatis.annotations.Mapper; + +/** + * 管理员Mapper + */ +@Mapper +public interface AdminMapper extends BaseMapper { +} diff --git a/src/main/java/com/it/rattan/monisuo/mapper/CoinMapper.java b/src/main/java/com/it/rattan/monisuo/mapper/CoinMapper.java new file mode 100644 index 0000000..7cee8a8 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/mapper/CoinMapper.java @@ -0,0 +1,12 @@ +package com.it.rattan.monisuo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.it.rattan.monisuo.entity.Coin; +import org.apache.ibatis.annotations.Mapper; + +/** + * 币种Mapper + */ +@Mapper +public interface CoinMapper extends BaseMapper { +} diff --git a/src/main/java/com/it/rattan/monisuo/mapper/OrderFundMapper.java b/src/main/java/com/it/rattan/monisuo/mapper/OrderFundMapper.java new file mode 100644 index 0000000..24c73b1 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/mapper/OrderFundMapper.java @@ -0,0 +1,23 @@ +package com.it.rattan.monisuo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.it.rattan.monisuo.entity.OrderFund; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; +import java.math.BigDecimal; + +/** + * 充提订单Mapper + */ +@Mapper +public interface OrderFundMapper extends BaseMapper { + + @Select("SELECT IFNULL(SUM(amount), 0) FROM order_fund WHERE type = 1 AND status = 2") + BigDecimal sumCompletedDeposit(); + + @Select("SELECT IFNULL(SUM(amount), 0) FROM order_fund WHERE type = 2 AND status = 2") + BigDecimal sumCompletedWithdraw(); + + @Select("SELECT COUNT(*) FROM order_fund WHERE status = 1") + int countPending(); +} diff --git a/src/main/java/com/it/rattan/monisuo/mapper/OrderTradeMapper.java b/src/main/java/com/it/rattan/monisuo/mapper/OrderTradeMapper.java new file mode 100644 index 0000000..738702b --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/mapper/OrderTradeMapper.java @@ -0,0 +1,12 @@ +package com.it.rattan.monisuo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.it.rattan.monisuo.entity.OrderTrade; +import org.apache.ibatis.annotations.Mapper; + +/** + * 交易订单Mapper + */ +@Mapper +public interface OrderTradeMapper extends BaseMapper { +} diff --git a/src/main/java/com/it/rattan/monisuo/mapper/UserMapper.java b/src/main/java/com/it/rattan/monisuo/mapper/UserMapper.java new file mode 100644 index 0000000..74dd803 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/mapper/UserMapper.java @@ -0,0 +1,12 @@ +package com.it.rattan.monisuo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.it.rattan.monisuo.entity.User; +import org.apache.ibatis.annotations.Mapper; + +/** + * 用户Mapper + */ +@Mapper +public interface UserMapper extends BaseMapper { +} diff --git a/src/main/java/com/it/rattan/monisuo/service/AssetService.java b/src/main/java/com/it/rattan/monisuo/service/AssetService.java new file mode 100644 index 0000000..d3c9933 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/service/AssetService.java @@ -0,0 +1,256 @@ +package com.it.rattan.monisuo.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.it.rattan.monisuo.entity.AccountFlow; +import com.it.rattan.monisuo.entity.AccountFund; +import com.it.rattan.monisuo.entity.AccountTrade; +import com.it.rattan.monisuo.entity.Coin; +import com.it.rattan.monisuo.mapper.AccountFlowMapper; +import com.it.rattan.monisuo.mapper.AccountFundMapper; +import com.it.rattan.monisuo.mapper.AccountTradeMapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.it.rattan.monisuo.util.OrderNoUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.util.*; + +/** + * 资产服务 + */ +@Service +public class AssetService { + + @Autowired + private AccountFundMapper accountFundMapper; + + @Autowired + private AccountTradeMapper accountTradeMapper; + + @Autowired + private AccountFlowMapper accountFlowMapper; + + @Autowired + private CoinService coinService; + + /** + * 获取资产总览 + */ + public Map getOverview(Long userId) { + Map result = new HashMap<>(); + + // 资金账户 + AccountFund fund = getOrCreateFundAccount(userId); + result.put("fundBalance", fund.getBalance()); + result.put("fundFrozen", fund.getFrozen()); + + // 交易账户 + BigDecimal tradeValue = BigDecimal.ZERO; + List> positions = new ArrayList<>(); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(AccountTrade::getUserId, userId) + .gt(AccountTrade::getQuantity, BigDecimal.ZERO); + List trades = accountTradeMapper.selectList(wrapper); + + for (AccountTrade trade : trades) { + Coin coin = coinService.getCoinByCode(trade.getCoinCode()); + if (coin != null) { + BigDecimal value = trade.getQuantity().multiply(coin.getPrice()) + .setScale(8, RoundingMode.DOWN); + tradeValue = tradeValue.add(value); + + Map position = new HashMap<>(); + position.put("coinCode", trade.getCoinCode()); + position.put("coinName", coin.getName()); + position.put("quantity", trade.getQuantity()); + position.put("price", coin.getPrice()); + position.put("value", value); + position.put("avgPrice", trade.getAvgPrice()); + positions.add(position); + } + } + + result.put("tradeValue", tradeValue); + result.put("positions", positions); + + // 总资产 + BigDecimal totalAssets = fund.getBalance().add(tradeValue); + result.put("totalAssets", totalAssets); + + return result; + } + + /** + * 获取资金账户 + */ + public AccountFund getOrCreateFundAccount(Long userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(AccountFund::getUserId, userId); + AccountFund fund = accountFundMapper.selectOne(wrapper); + + if (fund == null) { + fund = new AccountFund(); + fund.setUserId(userId); + fund.setBalance(BigDecimal.ZERO); + fund.setFrozen(BigDecimal.ZERO); + fund.setTotalDeposit(BigDecimal.ZERO); + fund.setTotalWithdraw(BigDecimal.ZERO); + fund.setCreateTime(LocalDateTime.now()); + accountFundMapper.insert(fund); + } + + return fund; + } + + /** + * 获取交易账户 + */ + public List> getTradeAccount(Long userId) { + List> result = new ArrayList<>(); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(AccountTrade::getUserId, userId) + .gt(AccountTrade::getQuantity, BigDecimal.ZERO); + List trades = accountTradeMapper.selectList(wrapper); + + for (AccountTrade trade : trades) { + Coin coin = coinService.getCoinByCode(trade.getCoinCode()); + if (coin != null) { + BigDecimal value = trade.getQuantity().multiply(coin.getPrice()) + .setScale(8, RoundingMode.DOWN); + + Map item = new HashMap<>(); + item.put("coinCode", trade.getCoinCode()); + item.put("coinName", coin.getName()); + item.put("coinIcon", coin.getIcon()); + item.put("quantity", trade.getQuantity()); + item.put("price", coin.getPrice()); + item.put("value", value); + item.put("avgPrice", trade.getAvgPrice()); + item.put("change24h", coin.getChange24h()); + result.add(item); + } + } + + return result; + } + + /** + * 资金划转 + */ + @Transactional + public void transfer(Long userId, Integer direction, BigDecimal amount) { + if (amount.compareTo(BigDecimal.ZERO) <= 0) { + throw new RuntimeException("划转金额必须大于0"); + } + + AccountFund fund = getOrCreateFundAccount(userId); + + // 获取交易账户USDT持仓 + AccountTrade tradeUsdt = getOrCreateTradeAccount(userId, "USDT"); + + if (direction == 1) { + // 资金账户 -> 交易账户 + if (fund.getBalance().compareTo(amount) < 0) { + throw new RuntimeException("资金账户余额不足"); + } + fund.setBalance(fund.getBalance().subtract(amount)); + tradeUsdt.setQuantity(tradeUsdt.getQuantity().add(amount)); + + // 记录流水 + createFlow(userId, 4, amount.negate(), fund.getBalance().add(amount), + fund.getBalance(), "USDT", null, "划转至交易账户"); + + } else if (direction == 2) { + // 交易账户 -> 资金账户 + if (tradeUsdt.getQuantity().compareTo(amount) < 0) { + throw new RuntimeException("交易账户USDT余额不足"); + } + tradeUsdt.setQuantity(tradeUsdt.getQuantity().subtract(amount)); + fund.setBalance(fund.getBalance().add(amount)); + + // 记录流水 + createFlow(userId, 3, amount, fund.getBalance().subtract(amount), + fund.getBalance(), "USDT", null, "划转至资金账户"); + + } else { + throw new RuntimeException("无效的划转方向"); + } + + fund.setUpdateTime(LocalDateTime.now()); + accountFundMapper.updateById(fund); + + tradeUsdt.setUpdateTime(LocalDateTime.now()); + accountTradeMapper.updateById(tradeUsdt); + } + + /** + * 获取或创建交易账户 + */ + public AccountTrade getOrCreateTradeAccount(Long userId, String coinCode) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(AccountTrade::getUserId, userId) + .eq(AccountTrade::getCoinCode, coinCode.toUpperCase()); + AccountTrade trade = accountTradeMapper.selectOne(wrapper); + + if (trade == null) { + trade = new AccountTrade(); + trade.setUserId(userId); + trade.setCoinCode(coinCode.toUpperCase()); + trade.setQuantity(BigDecimal.ZERO); + trade.setFrozen(BigDecimal.ZERO); + trade.setAvgPrice(BigDecimal.ZERO); + trade.setTotalBuy(BigDecimal.ZERO); + trade.setTotalSell(BigDecimal.ZERO); + trade.setCreateTime(LocalDateTime.now()); + accountTradeMapper.insert(trade); + } + + return trade; + } + + /** + * 创建资金流水 + */ + public void createFlow(Long userId, Integer flowType, BigDecimal amount, + BigDecimal balanceBefore, BigDecimal balanceAfter, + String coinCode, String relatedOrderNo, String remark) { + AccountFlow flow = new AccountFlow(); + flow.setUserId(userId); + flow.setFlowNo(OrderNoUtil.flowNo()); + flow.setFlowType(flowType); + flow.setAmount(amount); + flow.setBalanceBefore(balanceBefore); + flow.setBalanceAfter(balanceAfter); + flow.setCoinCode(coinCode); + flow.setRelatedOrderNo(relatedOrderNo); + flow.setRemark(remark); + flow.setCreateTime(LocalDateTime.now()); + accountFlowMapper.insert(flow); + } + + /** + * 获取资金流水 + */ + public List getFlows(Long userId, Integer flowType, int page, int size) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(AccountFlow::getUserId, userId); + if (flowType != null && flowType > 0) { + wrapper.eq(AccountFlow::getFlowType, flowType); + } + wrapper.orderByDesc(AccountFlow::getCreateTime); + wrapper.last("LIMIT " + (page - 1) * size + ", " + size); + return accountFlowMapper.selectList(wrapper); + } + + /** + * 更新资金账户 + */ + public void updateFundAccount(LambdaUpdateWrapper updateWrapper) { + accountFundMapper.update(null, updateWrapper); + } +} diff --git a/src/main/java/com/it/rattan/monisuo/service/CoinService.java b/src/main/java/com/it/rattan/monisuo/service/CoinService.java new file mode 100644 index 0000000..76bc1dc --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/service/CoinService.java @@ -0,0 +1,58 @@ +package com.it.rattan.monisuo.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.it.rattan.monisuo.entity.Coin; +import com.it.rattan.monisuo.mapper.CoinMapper; +import org.springframework.stereotype.Service; +import java.math.BigDecimal; +import java.util.List; + +/** + * 币种服务 + */ +@Service +public class CoinService extends ServiceImpl { + + /** + * 获取所有上架币种 + */ + public List getActiveCoins() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Coin::getStatus, 1) + .orderByDesc(Coin::getSort); + return list(wrapper); + } + + /** + * 根据代码获取币种 + */ + public Coin getCoinByCode(String code) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Coin::getCode, code.toUpperCase()) + .eq(Coin::getStatus, 1); + return getOne(wrapper); + } + + /** + * 更新币种价格 + */ + public void updatePrice(String code, BigDecimal price) { + Coin coin = getCoinByCode(code); + if (coin != null) { + coin.setPrice(price); + coin.setUpdateTime(java.time.LocalDateTime.now()); + updateById(coin); + } + } + + /** + * 获取实时币种列表 + */ + public List getRealTimeCoins() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Coin::getPriceType, 1) + .eq(Coin::getStatus, 1); + return list(wrapper); + } +} diff --git a/src/main/java/com/it/rattan/monisuo/service/FundService.java b/src/main/java/com/it/rattan/monisuo/service/FundService.java new file mode 100644 index 0000000..4d4ec86 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/service/FundService.java @@ -0,0 +1,244 @@ +package com.it.rattan.monisuo.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.it.rattan.monisuo.entity.AccountFund; +import com.it.rattan.monisuo.entity.OrderFund; +import com.it.rattan.monisuo.entity.User; +import com.it.rattan.monisuo.mapper.OrderFundMapper; +import com.it.rattan.monisuo.mapper.UserMapper; +import com.it.rattan.monisuo.util.OrderNoUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; + +/** + * 充提服务 + */ +@Service +public class FundService { + + @Autowired + private OrderFundMapper orderFundMapper; + + @Autowired + private AssetService assetService; + + @Autowired + private UserMapper userMapper; + + /** + * 申请充值 + */ + @Transactional + public Map deposit(Long userId, BigDecimal amount, String remark) { + if (amount.compareTo(BigDecimal.ZERO) <= 0) { + throw new RuntimeException("充值金额必须大于0"); + } + + User user = userMapper.selectById(userId); + if (user == null) { + throw new RuntimeException("用户不存在"); + } + + OrderFund order = new OrderFund(); + order.setOrderNo(OrderNoUtil.fundOrderNo()); + order.setUserId(userId); + order.setUsername(user.getUsername()); + order.setType(1); // 充值 + order.setAmount(amount); + order.setStatus(1); // 待审批 + order.setRemark(remark); + order.setCreateTime(LocalDateTime.now()); + orderFundMapper.insert(order); + + Map result = new HashMap<>(); + result.put("orderNo", order.getOrderNo()); + result.put("amount", amount); + result.put("status", order.getStatus()); + return result; + } + + /** + * 申请提现 + */ + @Transactional + public Map withdraw(Long userId, BigDecimal amount, String remark) { + if (amount.compareTo(BigDecimal.ZERO) <= 0) { + throw new RuntimeException("提现金额必须大于0"); + } + + User user = userMapper.selectById(userId); + if (user == null) { + throw new RuntimeException("用户不存在"); + } + + // 检查余额 + AccountFund fund = assetService.getOrCreateFundAccount(userId); + if (fund.getBalance().compareTo(amount) < 0) { + throw new RuntimeException("资金账户余额不足"); + } + + OrderFund order = new OrderFund(); + order.setOrderNo(OrderNoUtil.fundOrderNo()); + order.setUserId(userId); + order.setUsername(user.getUsername()); + order.setType(2); // 提现 + order.setAmount(amount); + order.setStatus(1); // 待审批 + order.setRemark(remark); + order.setCreateTime(LocalDateTime.now()); + orderFundMapper.insert(order); + + Map result = new HashMap<>(); + result.put("orderNo", order.getOrderNo()); + result.put("amount", amount); + result.put("status", order.getStatus()); + return result; + } + + /** + * 取消订单 + */ + @Transactional + public void cancel(Long userId, String orderNo) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(OrderFund::getUserId, userId) + .eq(OrderFund::getOrderNo, orderNo) + .eq(OrderFund::getStatus, 1); // 仅待审批可取消 + + OrderFund order = orderFundMapper.selectOne(wrapper); + if (order == null) { + throw new RuntimeException("订单不存在或状态不可取消"); + } + + order.setStatus(4); // 已取消 + order.setUpdateTime(LocalDateTime.now()); + orderFundMapper.updateById(order); + } + + /** + * 获取充提记录 + */ + public IPage getOrders(Long userId, Integer type, int pageNum, int pageSize) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(OrderFund::getUserId, userId); + if (type != null && type > 0) { + wrapper.eq(OrderFund::getType, type); + } + wrapper.orderByDesc(OrderFund::getCreateTime); + + Page page = new Page<>(pageNum, pageSize); + return orderFundMapper.selectPage(page, wrapper); + } + + /** + * 获取待审批订单数量 + */ + public int getPendingCount() { + return orderFundMapper.countPending(); + } + + /** + * 管理员审批 + */ + @Transactional + public void approve(Long adminId, String adminName, String orderNo, Integer status, + String rejectReason, String adminRemark) { + OrderFund order = orderFundMapper.selectOne( + new LambdaQueryWrapper().eq(OrderFund::getOrderNo, orderNo)); + + if (order == null) { + throw new RuntimeException("订单不存在"); + } + + if (order.getStatus() != 1) { + throw new RuntimeException("订单已处理"); + } + + if (status == 2) { + // 审批通过 + AccountFund fund = assetService.getOrCreateFundAccount(order.getUserId()); + + if (order.getType() == 1) { + // 充值:增加余额 + fund.setBalance(fund.getBalance().add(order.getAmount())); + fund.setTotalDeposit(fund.getTotalDeposit().add(order.getAmount())); + } else { + // 提现:扣减余额 + if (fund.getBalance().compareTo(order.getAmount()) < 0) { + throw new RuntimeException("用户余额不足"); + } + fund.setBalance(fund.getBalance().subtract(order.getAmount())); + fund.setTotalWithdraw(fund.getTotalWithdraw().add(order.getAmount())); + } + + fund.setUpdateTime(LocalDateTime.now()); + + // 更新账户 + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(AccountFund::getUserId, order.getUserId()) + .set(AccountFund::getBalance, fund.getBalance()) + .set(AccountFund::getTotalDeposit, fund.getTotalDeposit()) + .set(AccountFund::getTotalWithdraw, fund.getTotalWithdraw()) + .set(AccountFund::getUpdateTime, LocalDateTime.now()); + assetService.updateFundAccount(updateWrapper); + + // 记录流水 + int flowType = order.getType() == 1 ? 1 : 2; + String remark = order.getType() == 1 ? "充值" : "提现"; + assetService.createFlow(order.getUserId(), flowType, order.getAmount(), + fund.getBalance().subtract(order.getAmount()), + fund.getBalance(), "USDT", orderNo, remark); + + } else if (status == 3) { + // 审批驳回 + if (rejectReason == null || rejectReason.isEmpty()) { + throw new RuntimeException("请填写驳回原因"); + } + order.setRejectReason(rejectReason); + } + + order.setStatus(status); + order.setApproveAdminId(adminId); + order.setApproveAdminName(adminName); + order.setApproveTime(LocalDateTime.now()); + order.setAdminRemark(adminRemark); + order.setUpdateTime(LocalDateTime.now()); + orderFundMapper.updateById(order); + } + + /** + * 获取待审批订单列表 + */ + public IPage getPendingOrders(int pageNum, int pageSize) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(OrderFund::getStatus, 1) + .orderByAsc(OrderFund::getCreateTime); + + Page page = new Page<>(pageNum, pageSize); + return orderFundMapper.selectPage(page, wrapper); + } + + /** + * 获取所有充提订单(管理员) + */ + public IPage getAllOrders(Integer type, Integer status, int pageNum, int pageSize) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (type != null && type > 0) { + wrapper.eq(OrderFund::getType, type); + } + if (status != null && status > 0) { + wrapper.eq(OrderFund::getStatus, status); + } + wrapper.orderByDesc(OrderFund::getCreateTime); + + Page page = new Page<>(pageNum, pageSize); + return orderFundMapper.selectPage(page, wrapper); + } +} diff --git a/src/main/java/com/it/rattan/monisuo/service/TradeService.java b/src/main/java/com/it/rattan/monisuo/service/TradeService.java new file mode 100644 index 0000000..fba79f9 --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/service/TradeService.java @@ -0,0 +1,189 @@ +package com.it.rattan.monisuo.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.it.rattan.monisuo.entity.*; +import com.it.rattan.monisuo.mapper.OrderTradeMapper; +import com.it.rattan.monisuo.util.OrderNoUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.util.*; + +/** + * 交易服务 + */ +@Service +public class TradeService { + + @Autowired + private OrderTradeMapper orderTradeMapper; + + @Autowired + private AssetService assetService; + + @Autowired + private CoinService coinService; + + /** + * 买入 + */ + @Transactional + public Map buy(Long userId, String coinCode, BigDecimal price, BigDecimal quantity) { + Coin coin = coinService.getCoinByCode(coinCode); + if (coin == null) { + throw new RuntimeException("币种不存在"); + } + + if (coin.getStatus() != 1) { + throw new RuntimeException("该币种已下架"); + } + + // 计算金额 + BigDecimal amount = price.multiply(quantity).setScale(8, RoundingMode.DOWN); + + // 检查交易账户USDT余额 + AccountTrade usdtAccount = assetService.getOrCreateTradeAccount(userId, "USDT"); + if (usdtAccount.getQuantity().compareTo(amount) < 0) { + throw new RuntimeException("交易账户USDT余额不足,请先划转资金"); + } + + // 扣减USDT + usdtAccount.setQuantity(usdtAccount.getQuantity().subtract(amount)); + usdtAccount.setUpdateTime(LocalDateTime.now()); + + // 增加持仓 + AccountTrade coinAccount = assetService.getOrCreateTradeAccount(userId, coinCode); + BigDecimal oldTotal = coinAccount.getQuantity().multiply(coinAccount.getAvgPrice()); + BigDecimal newTotal = oldTotal.add(amount); + BigDecimal newQuantity = coinAccount.getQuantity().add(quantity); + + coinAccount.setQuantity(newQuantity); + if (newQuantity.compareTo(BigDecimal.ZERO) > 0) { + coinAccount.setAvgPrice(newTotal.divide(newQuantity, 8, RoundingMode.DOWN)); + } + coinAccount.setTotalBuy(coinAccount.getTotalBuy().add(quantity)); + coinAccount.setUpdateTime(LocalDateTime.now()); + + // 创建订单 + OrderTrade order = new OrderTrade(); + order.setOrderNo(OrderNoUtil.tradeOrderNo()); + order.setUserId(userId); + order.setCoinCode(coinCode.toUpperCase()); + order.setDirection(1); // 买入 + order.setOrderType(1); // 市价 + order.setPrice(price); + order.setQuantity(quantity); + order.setAmount(amount); + order.setFee(BigDecimal.ZERO); + order.setStatus(1); // 成功 + order.setCreateTime(LocalDateTime.now()); + orderTradeMapper.insert(order); + + // 记录流水 + assetService.createFlow(userId, 5, amount.negate(), usdtAccount.getQuantity().add(amount), + usdtAccount.getQuantity(), "USDT", order.getOrderNo(), + "买入" + coinCode + ",数量:" + quantity); + + Map result = new HashMap<>(); + result.put("orderNo", order.getOrderNo()); + result.put("price", price); + result.put("quantity", quantity); + result.put("amount", amount); + return result; + } + + /** + * 卖出 + */ + @Transactional + public Map sell(Long userId, String coinCode, BigDecimal price, BigDecimal quantity) { + Coin coin = coinService.getCoinByCode(coinCode); + if (coin == null) { + throw new RuntimeException("币种不存在"); + } + + if (coin.getStatus() != 1) { + throw new RuntimeException("该币种已下架"); + } + + // 检查持仓 + AccountTrade coinAccount = assetService.getOrCreateTradeAccount(userId, coinCode); + if (coinAccount.getQuantity().compareTo(quantity) < 0) { + throw new RuntimeException("持仓数量不足"); + } + + // 计算金额 + BigDecimal amount = price.multiply(quantity).setScale(8, RoundingMode.DOWN); + + // 扣减持仓 + coinAccount.setQuantity(coinAccount.getQuantity().subtract(quantity)); + coinAccount.setTotalSell(coinAccount.getTotalSell().add(quantity)); + coinAccount.setUpdateTime(LocalDateTime.now()); + + // 增加USDT + AccountTrade usdtAccount = assetService.getOrCreateTradeAccount(userId, "USDT"); + usdtAccount.setQuantity(usdtAccount.getQuantity().add(amount)); + usdtAccount.setUpdateTime(LocalDateTime.now()); + + // 创建订单 + OrderTrade order = new OrderTrade(); + order.setOrderNo(OrderNoUtil.tradeOrderNo()); + order.setUserId(userId); + order.setCoinCode(coinCode.toUpperCase()); + order.setDirection(2); // 卖出 + order.setOrderType(1); // 市价 + order.setPrice(price); + order.setQuantity(quantity); + order.setAmount(amount); + order.setFee(BigDecimal.ZERO); + order.setStatus(1); // 成功 + order.setCreateTime(LocalDateTime.now()); + orderTradeMapper.insert(order); + + // 记录流水 + assetService.createFlow(userId, 6, amount, usdtAccount.getQuantity().subtract(amount), + usdtAccount.getQuantity(), "USDT", order.getOrderNo(), + "卖出" + coinCode + ",数量:" + quantity); + + Map result = new HashMap<>(); + result.put("orderNo", order.getOrderNo()); + result.put("price", price); + result.put("quantity", quantity); + result.put("amount", amount); + return result; + } + + /** + * 获取交易记录 + */ + public IPage getOrders(Long userId, String coinCode, Integer direction, + int pageNum, int pageSize) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(OrderTrade::getUserId, userId); + if (coinCode != null && !coinCode.isEmpty()) { + wrapper.eq(OrderTrade::getCoinCode, coinCode.toUpperCase()); + } + if (direction != null && direction > 0) { + wrapper.eq(OrderTrade::getDirection, direction); + } + wrapper.orderByDesc(OrderTrade::getCreateTime); + + Page page = new Page<>(pageNum, pageSize); + return orderTradeMapper.selectPage(page, wrapper); + } + + /** + * 获取订单详情 + */ + public OrderTrade getOrderDetail(Long userId, String orderNo) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(OrderTrade::getUserId, userId) + .eq(OrderTrade::getOrderNo, orderNo); + return orderTradeMapper.selectOne(wrapper); + } +} diff --git a/src/main/java/com/it/rattan/monisuo/service/UserService.java b/src/main/java/com/it/rattan/monisuo/service/UserService.java new file mode 100644 index 0000000..d61ec3c --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/service/UserService.java @@ -0,0 +1,156 @@ +package com.it.rattan.monisuo.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.it.rattan.monisuo.context.UserContext; +import com.it.rattan.monisuo.entity.AccountFund; +import com.it.rattan.monisuo.entity.User; +import com.it.rattan.monisuo.mapper.AccountFundMapper; +import com.it.rattan.monisuo.mapper.UserMapper; +import com.it.rattan.monisuo.util.JwtUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +/** + * 用户服务 + */ +@Service +public class UserService extends ServiceImpl { + + @Autowired + private UserMapper userMapper; + + @Autowired + private AccountFundMapper accountFundMapper; + + private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + + /** + * 用户注册 + */ + @Transactional + public Map register(String username, String password) { + // 检查用户名是否存在 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getUsername, username); + if (userMapper.selectCount(wrapper) > 0) { + throw new RuntimeException("用户名已存在"); + } + + // 创建用户 + User user = new User(); + user.setUsername(username); + user.setPassword(passwordEncoder.encode(password)); + user.setNickname(username); + user.setKycStatus(0); + user.setStatus(1); + user.setCreateTime(LocalDateTime.now()); + userMapper.insert(user); + + // 初始化资金账户 + AccountFund fund = new AccountFund(); + fund.setUserId(user.getId()); + fund.setBalance(java.math.BigDecimal.ZERO); + fund.setFrozen(java.math.BigDecimal.ZERO); + fund.setTotalDeposit(java.math.BigDecimal.ZERO); + fund.setTotalWithdraw(java.math.BigDecimal.ZERO); + fund.setCreateTime(LocalDateTime.now()); + accountFundMapper.insert(fund); + + // 生成Token + String token = JwtUtil.createToken(user.getId(), username, "user"); + + // 更新Token + user.setToken(token); + userMapper.updateById(user); + + Map result = new HashMap<>(); + result.put("token", token); + result.put("userInfo", buildUserInfo(user)); + return result; + } + + /** + * 用户登录 + */ + public Map login(String username, String password) { + // 查询用户 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getUsername, username); + User user = userMapper.selectOne(wrapper); + + if (user == null) { + throw new RuntimeException("用户不存在"); + } + + if (user.getStatus() == 0) { + throw new RuntimeException("账号已被禁用"); + } + + if (!passwordEncoder.matches(password, user.getPassword())) { + throw new RuntimeException("密码错误"); + } + + // 生成新Token + String token = JwtUtil.createToken(user.getId(), username, "user"); + + // 更新登录信息 + user.setToken(token); + user.setLastLoginTime(LocalDateTime.now()); + userMapper.updateById(user); + + Map result = new HashMap<>(); + result.put("token", token); + result.put("userInfo", buildUserInfo(user)); + return result; + } + + /** + * 获取用户信息 + */ + public Map getUserInfo(Long userId) { + User user = userMapper.selectById(userId); + if (user == null) { + throw new RuntimeException("用户不存在"); + } + return buildUserInfo(user); + } + + /** + * 上传KYC资料 + */ + @Transactional + public void uploadKyc(Long userId, String idCardFront, String idCardBack) { + User user = userMapper.selectById(userId); + if (user == null) { + throw new RuntimeException("用户不存在"); + } + + user.setIdCardFront(idCardFront); + user.setIdCardBack(idCardBack); + user.setKycStatus(1); + user.setUpdateTime(LocalDateTime.now()); + userMapper.updateById(user); + } + + /** + * 构建用户信息返回 + */ + private Map buildUserInfo(User user) { + Map info = new HashMap<>(); + info.put("id", user.getId()); + info.put("username", user.getUsername()); + info.put("nickname", user.getNickname()); + info.put("avatar", user.getAvatar()); + info.put("phone", user.getPhone()); + info.put("email", user.getEmail()); + info.put("kycStatus", user.getKycStatus()); + info.put("status", user.getStatus()); + return info; + } +} diff --git a/src/main/java/com/it/rattan/monisuo/util/JwtUtil.java b/src/main/java/com/it/rattan/monisuo/util/JwtUtil.java new file mode 100644 index 0000000..78758aa --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/util/JwtUtil.java @@ -0,0 +1,103 @@ +package com.it.rattan.monisuo.util; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * JWT工具类 + */ +public class JwtUtil { + + /** 密钥 */ + private static final String SECRET = "monisuo_jwt_secret_key_2024"; + /** 过期时间(7天) */ + private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000L; + /** 签发者 */ + private static final String ISSUER = "monisuo"; + + /** + * 生成Token + */ + public static String createToken(Long userId, String username, String type) { + Date now = new Date(); + Date expireDate = new Date(now.getTime() + EXPIRE_TIME); + + Map header = new HashMap<>(); + header.put("alg", "HS256"); + header.put("typ", "JWT"); + + return JWT.create() + .withHeader(header) + .withIssuer(ISSUER) + .withIssuedAt(now) + .withExpiresAt(expireDate) + .withClaim("userId", userId) + .withClaim("username", username) + .withClaim("type", type) + .sign(Algorithm.HMAC256(SECRET)); + } + + /** + * 验证Token + */ + public static DecodedJWT verifyToken(String token) throws JWTVerificationException { + JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)) + .withIssuer(ISSUER) + .build(); + return verifier.verify(token); + } + + /** + * 获取用户ID + */ + public static Long getUserId(String token) { + try { + DecodedJWT jwt = verifyToken(token); + return jwt.getClaim("userId").asLong(); + } catch (JWTVerificationException e) { + return null; + } + } + + /** + * 获取用户名 + */ + public static String getUsername(String token) { + try { + DecodedJWT jwt = verifyToken(token); + return jwt.getClaim("username").asString(); + } catch (JWTVerificationException e) { + return null; + } + } + + /** + * 获取类型(user/admin) + */ + public static String getType(String token) { + try { + DecodedJWT jwt = verifyToken(token); + return jwt.getClaim("type").asString(); + } catch (JWTVerificationException e) { + return null; + } + } + + /** + * 检查Token是否有效 + */ + public static boolean isValid(String token) { + try { + verifyToken(token); + return true; + } catch (JWTVerificationException e) { + return false; + } + } +} diff --git a/src/main/java/com/it/rattan/monisuo/util/OrderNoUtil.java b/src/main/java/com/it/rattan/monisuo/util/OrderNoUtil.java new file mode 100644 index 0000000..a0b47db --- /dev/null +++ b/src/main/java/com/it/rattan/monisuo/util/OrderNoUtil.java @@ -0,0 +1,46 @@ +package com.it.rattan.monisuo.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 订单号生成工具 + */ +public class OrderNoUtil { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + private static final AtomicLong SEQUENCE = new AtomicLong(0); + + /** + * 生成订单号 + * 格式: 前缀 + 时间戳 + 6位序列号 + */ + public static String generate(String prefix) { + LocalDateTime now = LocalDateTime.now(); + String timestamp = now.format(FORMATTER); + long seq = SEQUENCE.getAndIncrement() % 1000000; + return prefix + timestamp + String.format("%06d", seq); + } + + /** + * 生成交易订单号 + */ + public static String tradeOrderNo() { + return generate("T"); + } + + /** + * 生成充提订单号 + */ + public static String fundOrderNo() { + return generate("F"); + } + + /** + * 生成流水号 + */ + public static String flowNo() { + return generate("L"); + } +} diff --git a/src/main/java/com/it/rattan/rpc/BaseResponse.java b/src/main/java/com/it/rattan/rpc/BaseResponse.java new file mode 100644 index 0000000..ca09ca3 --- /dev/null +++ b/src/main/java/com/it/rattan/rpc/BaseResponse.java @@ -0,0 +1,41 @@ +package com.it.rattan.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.Setter; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Getter +@Setter +public class BaseResponse { + + public static final String SUCCESS = "0000"; + + public static final String FAIL = "0001"; + + private String msg; + + private String code; + + public BaseResponse(final String code,final String msg){ + this.code = code; + this.msg = msg; + } + + public static BaseResponse success(){ + return new BaseResponse(BaseResponse.SUCCESS,""); + } + + public static BaseResponse fail(final String message){ + return new BaseResponse(BaseResponse.FAIL,message); + } + public static BaseResponse fail(final String code,final String message){ + return new BaseResponse(code,message); + } + + + + +} diff --git a/src/main/java/com/it/rattan/rpc/ObjectRestResponse.java b/src/main/java/com/it/rattan/rpc/ObjectRestResponse.java new file mode 100644 index 0000000..06a98fe --- /dev/null +++ b/src/main/java/com/it/rattan/rpc/ObjectRestResponse.java @@ -0,0 +1,31 @@ +package com.it.rattan.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.Setter; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Getter +@Setter +public class ObjectRestResponse extends BaseResponse { + + private T Data; + + public ObjectRestResponse(final T data){ + super(SUCCESS,""); + this.setData(data); + } + + public ObjectRestResponse(final T data,final String code,final String msg){ + super(code,msg); + this.setData(data); + } + + public ObjectRestResponse data(T data){ + this.setData(data); + return this; + } + +} diff --git a/src/main/java/com/it/rattan/rpc/RattanResponse.java b/src/main/java/com/it/rattan/rpc/RattanResponse.java new file mode 100644 index 0000000..dc4f5e9 --- /dev/null +++ b/src/main/java/com/it/rattan/rpc/RattanResponse.java @@ -0,0 +1,35 @@ +package com.it.rattan.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.it.rattan.enums.RattanMark; +import lombok.Getter; +import lombok.Setter; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Getter +@Setter +public class RattanResponse extends ObjectRestResponse { + + + public RattanResponse(T data) { + super(data); + } + + public RattanResponse(T data, String code, String msg) { + super(data, code, msg); + } + + public static RattanResponse success(){ + return new RattanResponse<>(RattanMark.NULL, SUCCESS,RattanMark.EMPTY); + } + + public static RattanResponse success(Object data){ + return new RattanResponse<>(data, SUCCESS,RattanMark.EMPTY); + } + + public static RattanResponse fail(String msg){ + return new RattanResponse<>(RattanMark.NULL, FAIL,msg); + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..3efd85c --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,33 @@ +server: + port: 5010 + +spring: + datasource: + username: monisuo + password: JPJ8wYicSGC8aRnk + url: jdbc:mysql://8.155.172.147:3306/monisuo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8 + driver-class-name: com.mysql.cj.jdbc.Driver + + +#mybatis-plus +mybatis-plus: + mapper-locations: classpath*:com/it/rattan/monisuo/mapper/*.xml + +bean-searcher: + packages: com.it.rattan.monisuo + params: + pagination: + start: 1 + ignore-case-key: + + +# mybatisplus代码生成器配置 +generator: + username: root + password: 123admin + driver: com.mysql.jdbc.Driver + url: jdbc:mysql://localhost:3306/spccloud?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8 + #dbTableList: #数据库的表,可多张(自己设置) + #- rt_company + prefix: + rt \ No newline at end of file diff --git a/src/main/resources/application-prd.yml b/src/main/resources/application-prd.yml new file mode 100644 index 0000000..e0eb602 --- /dev/null +++ b/src/main/resources/application-prd.yml @@ -0,0 +1,58 @@ +server: + port: 9010 + +spring: + datasource: + username: root + password: 897admin$$ + url: jdbc:mysql://47.97.10.240:3306/spccloud?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8 + type: com.alibaba.druid.pool.DruidDataSource + driver-class-name: com.mysql.jdbc.Driver + initialSize: 1 + minIdle: 3 + maxActive: 80 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 30000 + validationQuery: select 'x' + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + # 打开PSCache,并且指定每个连接上PSCache的大小 + poolPreparedStatements: true + maxPoolPreparedStatementPerConnectionSize: 20 + # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 + connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 + + +#mybatis-plus +mybatis-plus: + mapper-locations: classpath*:com.it.rattan.spccloud.mapper/*.xml + +spccloud: + pdf-download-path: C:/Users/Administrator/Desktop/temp/ + wkhtmltopdf-exe-path: C:/Users/Administrator/Desktop/wkhtmltopdf/bin/wkhtmltopdf.exe + + +bean-searcher: + packages: com.rattan.spccloud + params: + pagination: + start: 1 + ignore-case-key: + + + +# mybatisplus代码生成器配置 +#generator: + #username: root + #password: 123admin + #driver: com.mysql.jdbc.Driver + #url: jdbc:mysql://47.97.10.240:3306/spccloud?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8 + #dbTableList: #数据库的表,可多张(自己设置) + #- rt_company + #prefix: + #rt \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..76d483d --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,5 @@ +spring: + profiles: + active: dev + + diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..6d1a589 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,16 @@ + + + logback + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + +