fzzj il y a 9 mois
Parent
révision
b72bd3868b
30 fichiers modifiés avec 2110 ajouts et 26 suppressions
  1. 111
    1
      RuoYi-App/App.vue
  2. 119
    0
      RuoYi-App/components/custom-tabbar/index.vue
  3. 304
    0
      RuoYi-App/pages/admin/upload.vue
  4. 174
    0
      RuoYi-App/pages/author/apply.vue
  5. 256
    14
      RuoYi-App/pages/novel/list.vue
  6. 155
    0
      RuoYi-App/pages/novel/read.vue
  7. 80
    0
      RuoYi-App/services/api.js
  8. 14
    0
      RuoYi-Vue/ruoyi-system/pom.xml
  9. 74
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/config/SecurityConfig.java
  10. 76
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/controller/AdminNovelController.java
  11. 89
    3
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/controller/NovelController.java
  12. 36
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/AuthorApplication.java
  13. 30
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/AuthorApplicationDTO.java
  14. 31
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/Chapter.java
  15. 9
    3
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/Novel.java
  16. 1
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/NovelChapter.java
  17. 14
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/AuthorApplicationMapper.java
  18. 10
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/CategoryMapper.java
  19. 9
    1
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/ChapterMapper.java
  20. 9
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/NovelMapper.java
  21. 15
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/AuthorApplicationRepository.java
  22. 9
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/CategoryRepository.java
  23. 17
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/ChapterRepository.java
  24. 19
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/NovelRepository.java
  25. 12
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/UserRepository.java
  26. 42
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/AuthServiceImpl.java
  27. 297
    4
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/NovelServiceImpl.java
  28. 52
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/utils/JwtTokenProvider.java
  29. 3
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java
  30. 43
    0
      RuoYi-Vue/ruoyi-system/src/main/resources/mapper/novel/NovelMapper.xml

+ 111
- 1
RuoYi-App/App.vue Voir le fichier

@@ -1,14 +1,85 @@
1 1
 <template>
2 2
   <view class="app-container">
3 3
     <router-view />
4
+    <!-- 若依风格自定义菜单 -->
5
+    <view v-if="showMenu" class="ruoyi-tabbar">
6
+      <view 
7
+        v-for="(item, index) in menuItems" 
8
+        :key="index"
9
+        class="tab-item"
10
+        :class="{ active: activeIndex === index }"
11
+        @click="switchTab(item, index)"
12
+      >
13
+        <image class="icon" :src="activeIndex === index ? item.activeIcon : item.icon" />
14
+        <text class="text">{{ item.text }}</text>
15
+      </view>
16
+    </view>
4 17
   </view>
5 18
 </template>
6 19
 
7 20
 <script>
21
+import CustomTabbar from '@/components/custom-tabbar/index.vue'
8 22
 import config from './config'
9 23
 import { getToken } from '@/utils/auth'
10 24
 
11 25
 export default {
26
+  components: { CustomTabbar },
27
+  data() {
28
+    return {
29
+      activeIndex: 0,
30
+      showMenu: true,
31
+      menuItems: [
32
+        {
33
+          path: '/pages/index/index',
34
+          icon: '/static/tabbar/home.png',
35
+          activeIcon: '/static/tabbar/home_selected.png',
36
+          text: '首页'
37
+        },
38
+        {
39
+          path: '/pages/novel/list',
40
+          icon: '/static/tabbar/novel.png',
41
+          activeIcon: '/static/tabbar/novel_selected.png',
42
+          text: '小说'
43
+        },
44
+        {
45
+          path: '/pages/bookshelf/index',
46
+          icon: '/static/tabbar/bookshelf.png',
47
+          activeIcon: '/static/tabbar/bookshelf_selected.png',
48
+          text: '书架'
49
+        },
50
+        {
51
+          path: '/pages/me/index',
52
+          icon: '/static/tabbar/mine.png',
53
+          activeIcon: '/static/tabbar/mine_selected.png',
54
+          text: '我的'
55
+        }
56
+      ]
57
+    }
58
+  },
59
+  watch: {
60
+    '$route.path': {
61
+      immediate: true,
62
+      handler(path) {
63
+        // 确定当前激活的菜单项
64
+        this.activeIndex = this.menuItems.findIndex(item => 
65
+          path.startsWith(item.path)
66
+        );
67
+        
68
+        // 决定是否显示菜单
69
+        this.showMenu = this.activeIndex !== -1;
70
+      }
71
+    }
72
+  },
73
+  methods: {
74
+    switchTab(item, index) {
75
+      if (this.activeIndex === index) return;
76
+      
77
+      this.activeIndex = index;
78
+      uni.switchTab({
79
+        url: item.path
80
+      });
81
+    }
82
+  },
12 83
   onLaunch() {
13 84
     // 初始化主题
14 85
     this.initTheme()
@@ -21,7 +92,7 @@ export default {
21 92
     // 检查登录状态
22 93
     //this.checkLogin()
23 94
       console.log('App launched, store:', this.$store)
24
-      console.log('TabBar config:', uni.getTabBar())
95
+
25 96
 
26 97
   },
27 98
   onShow() {
@@ -146,6 +217,45 @@ export default {
146 217
 </script>
147 218
 
148 219
 <style lang="scss">
220
+/* 确保自定义TabBar有足够的空间 */
221
+.app-container {
222
+  padding-bottom: 100rpx;
223
+}
224
+.ruoyi-tabbar {
225
+  position: fixed;
226
+  bottom: 0;
227
+  left: 0;
228
+  right: 0;
229
+  display: flex;
230
+  height: 100rpx;
231
+  background-color: #fff;
232
+  box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
233
+  z-index: 9999;
234
+  border-top: 1rpx solid #eee;
235
+  
236
+  .tab-item {
237
+    flex: 1;
238
+    display: flex;
239
+    flex-direction: column;
240
+    align-items: center;
241
+    justify-content: center;
242
+    
243
+    .icon {
244
+      width: 44rpx;
245
+      height: 44rpx;
246
+      margin-bottom: 6rpx;
247
+    }
248
+    
249
+    .text {
250
+      font-size: 22rpx;
251
+      color: #666;
252
+    }
253
+        .active .text {
254
+          color: #3a8ee6; /* 若依主题蓝色 */
255
+          font-weight: 500;
256
+        }  
257
+	}
258
+}
149 259
 /* 确保tabbar显示 */
150 260
 uni-tabbar {
151 261
   display: flex !important;

+ 119
- 0
RuoYi-App/components/custom-tabbar/index.vue Voir le fichier

@@ -0,0 +1,119 @@
1
+<template>
2
+  <view class="custom-tabbar">
3
+    <view 
4
+      v-for="(item, index) in list" 
5
+      :key="index"
6
+      class="custom-tabbar-item"
7
+      :class="{ 'custom-tabbar-item-active': selectedIndex === index }"
8
+      @click="switchTab(item, index)"
9
+    >
10
+      <image 
11
+        :src="selectedIndex === index ? item.selectedIcon : item.icon" 
12
+        class="custom-tabbar-icon"
13
+      />
14
+      <text class="custom-tabbar-text">{{ item.text }}</text>
15
+    </view>
16
+  </view>
17
+</template>
18
+
19
+<script>
20
+export default {
21
+  data() {
22
+    return {
23
+      selectedIndex: 0,
24
+      list: [
25
+        {
26
+          pagePath: "/pages/index/index",
27
+          icon: "/static/tabbar/home.png",
28
+          selectedIcon: "/static/tabbar/home_selected.png",
29
+          text: "首页"
30
+        },
31
+        {
32
+          pagePath: "/pages/novel/list",
33
+          icon: "/static/tabbar/novel.png",
34
+          selectedIcon: "/static/tabbar/novel_selected.png",
35
+          text: "小说"
36
+        },
37
+        {
38
+          pagePath: "/pages/bookshelf/index",
39
+          icon: "/static/tabbar/bookshelf.png",
40
+          selectedIcon: "/static/tabbar/bookshelf_selected.png",
41
+          text: "书架"
42
+        },
43
+        {
44
+          pagePath: "/pages/me/index",
45
+          icon: "/static/tabbar/mine.png",
46
+          selectedIcon: "/static/tabbar/mine_selected.png",
47
+          text: "我的"
48
+        }
49
+      ]
50
+    }
51
+  },
52
+  created() {
53
+    this.updateSelectedIndex();
54
+  },
55
+  methods: {
56
+    switchTab(item, index) {
57
+      if (this.selectedIndex === index) return;
58
+      
59
+      this.selectedIndex = index;
60
+      uni.switchTab({
61
+        url: item.pagePath
62
+      });
63
+    },
64
+    updateSelectedIndex() {
65
+      const pages = getCurrentPages();
66
+      if (!pages.length) return;
67
+      
68
+      const currentPage = pages[pages.length - 1];
69
+      const currentRoute = currentPage.route;
70
+      
71
+      const index = this.list.findIndex(item => 
72
+        item.pagePath.includes(currentRoute)
73
+      );
74
+      
75
+      if (index !== -1) {
76
+        this.selectedIndex = index;
77
+      }
78
+    }
79
+  }
80
+}
81
+</script>
82
+
83
+<style scoped>
84
+.custom-tabbar {
85
+  position: fixed;
86
+  bottom: 0;
87
+  left: 0;
88
+  right: 0;
89
+  display: flex;
90
+  height: 100rpx;
91
+  background-color: #ffffff;
92
+  box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
93
+  z-index: 9999;
94
+}
95
+
96
+.custom-tabbar-item {
97
+  flex: 1;
98
+  display: flex;
99
+  flex-direction: column;
100
+  align-items: center;
101
+  justify-content: center;
102
+}
103
+
104
+.custom-tabbar-icon {
105
+  width: 48rpx;
106
+  height: 48rpx;
107
+  margin-bottom: 6rpx;
108
+}
109
+
110
+.custom-tabbar-text {
111
+  font-size: 24rpx;
112
+  color: #7a7e83;
113
+}
114
+
115
+.custom-tabbar-item-active .custom-tabbar-text {
116
+  color: #3cc51f;
117
+  font-weight: bold;
118
+}
119
+</style>

+ 304
- 0
RuoYi-App/pages/admin/upload.vue Voir le fichier

@@ -0,0 +1,304 @@
1
+<template>
2
+  <view class="upload-container">
3
+    <view class="header">作品管理</view>
4
+    
5
+    <view class="tabs">
6
+      <view 
7
+        :class="{ active: activeTab === 'novel' }"
8
+        @click="activeTab = 'novel'"
9
+      >
10
+        上传整本小说
11
+      </view>
12
+      <view 
13
+        :class="{ active: activeTab === 'chapter' }"
14
+        @click="activeTab = 'chapter'"
15
+      >
16
+        上传章节
17
+      </view>
18
+    </view>
19
+    
20
+    <!-- 整本小说上传 -->
21
+    <view v-if="activeTab === 'novel'" class="form-section">
22
+      <view class="form-item">
23
+        <text class="label">小说标题</text>
24
+        <input v-model="novelForm.title" placeholder="请输入小说标题" />
25
+      </view>
26
+      
27
+      <view class="form-item">
28
+        <text class="label">选择作者</text>
29
+        <picker @change="bindAuthorChange" :value="authorIndex" :range="authors">
30
+          <view class="picker">
31
+            {{ novelForm.authorName || '请选择作者' }}
32
+          </view>
33
+        </picker>
34
+      </view>
35
+      
36
+      <view class="form-item">
37
+        <text class="label">上传文件</text>
38
+        <button class="upload-btn" @click="chooseNovelFile">选择TXT文件</button>
39
+        <text v-if="novelForm.file" class="file-name">{{ novelForm.file.name }}</text>
40
+      </view>
41
+      
42
+      <button class="submit-btn" @click="submitNovel">提交上传</button>
43
+    </view>
44
+    
45
+    <!-- 章节上传 -->
46
+    <view v-if="activeTab === 'chapter'" class="form-section">
47
+      <view class="form-item">
48
+        <text class="label">选择小说</text>
49
+        <picker @change="bindNovelChange" :value="novelIndex" :range="novels">
50
+          <view class="picker">
51
+            {{ chapterForm.novelTitle || '请选择小说' }}
52
+          </view>
53
+        </picker>
54
+      </view>
55
+      
56
+      <view class="form-item">
57
+        <text class="label">章节序号</text>
58
+        <input v-model="chapterForm.chapterOrder" placeholder="自动生成或手动指定" />
59
+      </view>
60
+      
61
+      <view class="form-item">
62
+        <text class="label">上传章节</text>
63
+        <button class="upload-btn" @click="chooseChapterFile">选择TXT文件</button>
64
+        <text v-if="chapterForm.file" class="file-name">{{ chapterForm.file.name }}</text>
65
+      </view>
66
+      
67
+      <button class="submit-btn" @click="submitChapter">提交上传</button>
68
+    </view>
69
+  </view>
70
+</template>
71
+
72
+<script>
73
+export default {
74
+  data() {
75
+    return {
76
+      activeTab: 'novel',
77
+      authors: [],
78
+      authorIndex: -1,
79
+      novels: [],
80
+      novelIndex: -1,
81
+      novelForm: {
82
+        title: '',
83
+        authorId: null,
84
+        authorName: '',
85
+        file: null
86
+      },
87
+      chapterForm: {
88
+        novelId: null,
89
+        novelTitle: '',
90
+        chapterOrder: '',
91
+        file: null
92
+      }
93
+    };
94
+  },
95
+  async onLoad() {
96
+    // 加载作者列表
97
+    const authorRes = await this.$api.getAuthorList();
98
+    this.authors = authorRes.data.map(a => a.nickName);
99
+    
100
+    // 加载小说列表
101
+    const novelRes = await this.$api.getNovelList();
102
+    this.novels = novelRes.rows.map(n => n.title);
103
+  },
104
+  methods: {
105
+    // 作者选择
106
+    bindAuthorChange(e) {
107
+      this.authorIndex = e.detail.value;
108
+      this.novelForm.authorId = this.authors[this.authorIndex].userId;
109
+      this.novelForm.authorName = this.authors[this.authorIndex].nickName;
110
+    },
111
+    
112
+    // 小说选择
113
+    bindNovelChange(e) {
114
+      this.novelIndex = e.detail.value;
115
+      this.chapterForm.novelId = this.novels[this.novelIndex].id;
116
+      this.chapterForm.novelTitle = this.novels[this.novelIndex].title;
117
+    },
118
+    
119
+    // 选择文件
120
+    chooseNovelFile() {
121
+      uni.chooseFile({
122
+        count: 1,
123
+        extension: ['.txt'],
124
+        success: (res) => {
125
+          this.novelForm.file = res.tempFiles[0];
126
+        }
127
+      });
128
+    },
129
+    
130
+    chooseChapterFile() {
131
+      uni.chooseFile({
132
+        count: 1,
133
+        extension: ['.txt'],
134
+        success: (res) => {
135
+          this.chapterForm.file = res.tempFiles[0];
136
+        }
137
+      });
138
+    },
139
+    
140
+    // 提交表单
141
+    async submitNovel() {
142
+      if (!this.novelForm.title || !this.novelForm.authorId || !this.novelForm.file) {
143
+        uni.showToast({ title: '请填写完整信息', icon: 'none' });
144
+        return;
145
+      }
146
+      
147
+      try {
148
+        const formData = new FormData();
149
+        formData.append('title', this.novelForm.title);
150
+        formData.append('authorId', this.novelForm.authorId);
151
+        formData.append('file', this.novelForm.file);
152
+        
153
+        const res = await this.$api.uploadNovel(formData);
154
+        uni.showToast({ title: '上传成功' });
155
+        this.resetNovelForm();
156
+      } catch (e) {
157
+        uni.showToast({ title: '上传失败: ' + e.message, icon: 'none' });
158
+      }
159
+    },
160
+    
161
+    async submitChapter() {
162
+      if (!this.chapterForm.novelId || !this.chapterForm.file) {
163
+        uni.showToast({ title: '请填写完整信息', icon: 'none' });
164
+        return;
165
+      }
166
+      
167
+      try {
168
+        const formData = new FormData();
169
+        formData.append('novelId', this.chapterForm.novelId);
170
+        if (this.chapterForm.chapterOrder) {
171
+          formData.append('chapterOrder', this.chapterForm.chapterOrder);
172
+        }
173
+        formData.append('file', this.chapterForm.file);
174
+        
175
+        const res = await this.$api.uploadChapter(formData);
176
+        uni.showToast({ title: '章节上传成功' });
177
+        this.resetChapterForm();
178
+      } catch (e) {
179
+        uni.showToast({ title: '上传失败: ' + e.message, icon: 'none' });
180
+      }
181
+    },
182
+    
183
+    resetNovelForm() {
184
+      this.novelForm = {
185
+        title: '',
186
+        authorId: null,
187
+        authorName: '',
188
+        file: null
189
+      };
190
+      this.authorIndex = -1;
191
+    },
192
+    
193
+    resetChapterForm() {
194
+      this.chapterForm = {
195
+        novelId: null,
196
+        novelTitle: '',
197
+        chapterOrder: '',
198
+        file: null
199
+      };
200
+      this.novelIndex = -1;
201
+    }
202
+  }
203
+}
204
+</script>
205
+
206
+<style lang="scss" scoped>
207
+.upload-container {
208
+  padding: 30rpx;
209
+}
210
+
211
+.header {
212
+  font-size: 36rpx;
213
+  font-weight: bold;
214
+  margin-bottom: 40rpx;
215
+  text-align: center;
216
+  color: #3a8ee6; /* 若依主题色 */
217
+}
218
+
219
+.tabs {
220
+  display: flex;
221
+  margin-bottom: 30rpx;
222
+  border-bottom: 1rpx solid #eee;
223
+  
224
+  view {
225
+    flex: 1;
226
+    text-align: center;
227
+    padding: 20rpx 0;
228
+    font-size: 30rpx;
229
+    color: #666;
230
+    
231
+    &.active {
232
+      color: #3a8ee6;
233
+      font-weight: bold;
234
+      border-bottom: 4rpx solid #3a8ee6;
235
+    }
236
+  }
237
+}
238
+
239
+.form-section {
240
+  background: #fff;
241
+  border-radius: 16rpx;
242
+  padding: 30rpx;
243
+  box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
244
+}
245
+
246
+.form-item {
247
+  margin-bottom: 40rpx;
248
+  padding-bottom: 30rpx;
249
+  border-bottom: 1rpx solid #f5f5f5;
250
+  
251
+  .label {
252
+    display: block;
253
+    font-size: 28rpx;
254
+    margin-bottom: 20rpx;
255
+    color: #333;
256
+    font-weight: 500;
257
+  }
258
+  
259
+  input, .picker {
260
+    width: 100%;
261
+    height: 80rpx;
262
+    font-size: 28rpx;
263
+    padding: 0 20rpx;
264
+    border: 1rpx solid #ddd;
265
+    border-radius: 8rpx;
266
+    box-sizing: border-box;
267
+  }
268
+  
269
+  .picker {
270
+    display: flex;
271
+    align-items: center;
272
+  }
273
+}
274
+
275
+.upload-btn {
276
+  background: #f8f8f8;
277
+  color: #3a8ee6;
278
+  font-size: 26rpx;
279
+  height: 80rpx;
280
+  line-height: 80rpx;
281
+  border: 1rpx dashed #3a8ee6;
282
+  border-radius: 8rpx;
283
+}
284
+
285
+.file-name {
286
+  display: block;
287
+  margin-top: 15rpx;
288
+  font-size: 26rpx;
289
+  color: #666;
290
+  overflow: hidden;
291
+  text-overflow: ellipsis;
292
+  white-space: nowrap;
293
+}
294
+
295
+.submit-btn {
296
+  background: #3a8ee6;
297
+  color: white;
298
+  height: 90rpx;
299
+  line-height: 90rpx;
300
+  border-radius: 45rpx;
301
+  font-size: 32rpx;
302
+  margin-top: 50rpx;
303
+}
304
+</style>

+ 174
- 0
RuoYi-App/pages/author/apply.vue Voir le fichier

@@ -0,0 +1,174 @@
1
+<template>
2
+  <view class="apply-container">
3
+    <view class="form-title">成为签约作家</view>
4
+    
5
+    <form @submit="submitForm">
6
+      <view class="form-item">
7
+        <text class="form-label">真实姓名</text>
8
+        <input v-model="form.realName" placeholder="请输入真实姓名" />
9
+      </view>
10
+      
11
+      <view class="form-item">
12
+        <text class="form-label">联系方式</text>
13
+        <input v-model="form.contact" placeholder="手机号/微信/QQ" />
14
+      </view>
15
+      
16
+  <!-- 添加作品类型选择器 -->
17
+  <view class="form-item">
18
+    <text class="form-label">作品类型</text>
19
+    <picker @change="bindTypeChange" :value="typeIndex" :range="novelTypes">
20
+      <view class="picker">
21
+        {{ form.type || '请选择作品类型' }}
22
+      </view>
23
+    </picker>
24
+  </view>
25
+      
26
+      <view class="form-item">
27
+        <text class="form-label">作品名称</text>
28
+        <input v-model="form.novelTitle" placeholder="请输入作品名称" />
29
+      </view>
30
+      
31
+      <view class="form-item">
32
+        <text class="form-label">作品简介</text>
33
+        <textarea 
34
+          v-model="form.description" 
35
+          placeholder="请简要描述你的作品内容" 
36
+          maxlength="500" 
37
+          auto-height
38
+        />
39
+      </view>
40
+      
41
+      <view class="form-item">
42
+        <text class="form-label">作品示例</text>
43
+        <textarea 
44
+          v-model="form.sampleContent" 
45
+          placeholder="请提供1-3章示例内容" 
46
+          maxlength="5000" 
47
+          auto-height
48
+        />
49
+      </view>
50
+      
51
+  <!-- 添加提交按钮状态 -->
52
+  <button 
53
+    form-type="submit" 
54
+    class="submit-btn"
55
+    :disabled="submitting"
56
+  >
57
+    {{ submitting ? '提交中...' : '提交申请' }}
58
+  </button>
59
+    </form>
60
+    
61
+    <view class="agreement">
62
+      提交申请即表示同意
63
+      <text class="link" @click="viewAgreement">《作家签约协议》</text>
64
+    </view>
65
+  </view>
66
+</template>
67
+
68
+<script>
69
+export default {
70
+  data() {
71
+    return {
72
+      form: {
73
+        realName: '',
74
+        contact: '',
75
+        type: '',
76
+        novelTitle: '',
77
+        description: '',
78
+        sampleContent: ''
79
+      },
80
+      novelTypes: ['都市言情', '玄幻奇幻', '武侠仙侠', '历史军事', '科幻灵异', '游戏竞技', '其他'],
81
+	  submitting: false,
82
+      typeIndex: -1
83
+    }
84
+  },
85
+  methods: {
86
+    bindTypeChange(e) {
87
+      this.typeIndex = e.detail.value;
88
+      this.form.type = this.novelTypes[this.typeIndex];
89
+    },
90
+    async submitForm() {
91
+		this.submitting = true;
92
+      // 表单验证
93
+      if (!this.form.realName || !this.form.contact || !this.form.type || 
94
+          !this.form.novelTitle || !this.form.description) {
95
+        uni.showToast({ title: '请填写完整信息', icon: 'none' });
96
+        return;
97
+      }
98
+      
99
+      try {
100
+        // 调用API
101
+        await this.$api.applyAuthor(this.form);
102
+        
103
+        uni.showToast({ title: '提交成功,请等待审核' });
104
+        setTimeout(() => {
105
+          uni.navigateBack();
106
+        }, 1500);
107
+      } catch (e) {
108
+        uni.showToast({ title: e.message || '提交失败', icon: 'none' });
109
+      } finally {
110
+        this.submitting = false;
111
+      }
112
+    },
113
+    viewAgreement() {
114
+      uni.navigateTo({
115
+        url: '/pages/agreement/author'
116
+      });
117
+    }
118
+  }
119
+}
120
+</script>
121
+
122
+<style scoped>
123
+.apply-container {
124
+  padding: 40rpx;
125
+}
126
+
127
+.form-title {
128
+  font-size: 40rpx;
129
+  font-weight: bold;
130
+  text-align: center;
131
+  margin-bottom: 40rpx;
132
+}
133
+
134
+.form-item {
135
+  margin-bottom: 30rpx;
136
+  padding-bottom: 20rpx;
137
+  border-bottom: 1rpx solid #eee;
138
+}
139
+
140
+.form-label {
141
+  display: block;
142
+  font-size: 30rpx;
143
+  margin-bottom: 15rpx;
144
+  color: #333;
145
+}
146
+
147
+input, textarea, .picker {
148
+  width: 100%;
149
+  font-size: 28rpx;
150
+  min-height: 60rpx;
151
+}
152
+
153
+textarea {
154
+  min-height: 200rpx;
155
+}
156
+
157
+.submit-btn {
158
+  background-color: #3cc51f;
159
+  color: white;
160
+  margin-top: 40rpx;
161
+  border-radius: 50rpx;
162
+}
163
+
164
+.agreement {
165
+  text-align: center;
166
+  margin-top: 30rpx;
167
+  font-size: 24rpx;
168
+  color: #888;
169
+}
170
+
171
+.link {
172
+  color: #3cc51f;
173
+}
174
+</style>

+ 256
- 14
RuoYi-App/pages/novel/list.vue Voir le fichier

@@ -1,19 +1,67 @@
1 1
 <template>
2
-  <view>
2
+  <view class="novel-list-page">
3 3
     <!-- 顶部广告(来自Java后台) -->
4 4
     <ad-banner :ads="topAds" />
5 5
     
6 6
     <!-- 小说列表 -->
7
-    <view class="novel-list">
8
-      <view v-for="novel in novelList" :key="novel.id" @click="openNovel(novel)">
9
-        <image :src="novel.cover" mode="aspectFill" />
10
-        <text>{{ novel.title }}</text>
7
+    <!-- 顶部分类导航 -->
8
+    <scroll-view scroll-x class="category-nav">
9
+      <view 
10
+        v-for="category in categories" 
11
+        :key="category.id"
12
+        class="category-item"
13
+        :class="{ active: activeCategory === category.id }"
14
+        @click="changeCategory(category.id)"
15
+      >
16
+        {{ category.name }}
11 17
       </view>
18
+    </scroll-view>
19
+    <!-- 热门推荐 -->
20
+    <view class="section">
21
+      <view class="section-header">
22
+        <text class="section-title">热门推荐</text>
23
+        <text class="section-more">更多 ></text>
24
+      </view>
25
+      <scroll-view scroll-x class="hot-list">
26
+        <view 
27
+          v-for="novel in hotNovels" 
28
+          :key="novel.id" 
29
+          class="hot-item"
30
+          @click="openNovel(novel)"
31
+        >
32
+          <image :src="novel.cover" class="hot-cover" />
33
+          <text class="hot-title">{{ novel.title }}</text>
34
+        </view>
35
+      </scroll-view>
12 36
     </view>
13 37
     
14
-    <!-- 底部广告 -->
15
-    <ad-banner :ads="bottomAds" />
38
+    <!-- 分类小说列表 -->
39
+    <view class="section">
40
+      <view class="section-header">
41
+        <text class="section-title">{{ currentCategoryName }}作品</text>
42
+      </view>
43
+      <view class="novel-grid">
44
+        <view 
45
+          v-for="novel in novels" 
46
+          :key="novel.id" 
47
+          class="novel-item"
48
+          @click="openNovel(novel)"
49
+        >
50
+          <image :src="novel.cover" class="novel-cover" />
51
+          <text class="novel-title">{{ novel.title }}</text>
52
+          <text class="novel-author">{{ novel.author }}</text>
53
+        </view>
54
+      </view>
55
+    </view>
56
+    
57
+    <!-- 成为签约作家按钮 -->
58
+    <view class="become-author" @click="applyAuthor">
59
+      <text>成为签约作家,发布你的作品</text>
60
+    </view>
61
+	    <!-- 底部广告 -->
62
+	    <ad-banner :ads="bottomAds" />
16 63
   </view>
64
+
17 65
 </template>
18 66
 <script>
19 67
 import AdBanner from '@/components/AdBanner';
@@ -24,12 +72,21 @@ export default {
24 72
     return {
25 73
       novelList: [], // 存储小说目录数据
26 74
 	        topAds: [],
27
-	        bottomAds: []
75
+	        bottomAds: [],
76
+			      categories: [],
77
+			      activeCategory: 0,
78
+			      hotNovels: [],
79
+			      novels: [],
80
+			      currentCategoryName: '全部'
81
+			
28 82
     }
29 83
   },
30 84
   async onLoad() {
31 85
     await this.loadAds();
32 86
     await this.loadNovels();
87
+	    this.loadCategories();
88
+	    this.loadHotNovels();
89
+	    // this.loadNovels();
33 90
   },
34 91
   // onLoad() {
35 92
   //   this.loadNovelList();
@@ -45,13 +102,72 @@ export default {
45 102
       this.topAds = topRes.data;
46 103
       this.bottomAds = bottomRes.data;
47 104
     },
48
-    
105
+	    applyAuthor() {
106
+	      if (!this.$store.getters.token) {
107
+	        uni.showToast({ title: '请先登录', icon: 'none' });
108
+	        uni.navigateTo({ url: '/pages/login' });
109
+	        return;
110
+	      }
111
+	      
112
+	      uni.navigateTo({ url: '/pages/author/apply' });
113
+	    },
114
+        async loadCategories() {
115
+          try {
116
+            // 从Java后台获取分类
117
+            const res = await this.$http.get('/novel/category/list');
118
+            this.categories = [{ id: 0, name: '全部' }, ...res.data];
119
+          } catch (e) {
120
+            console.error('加载分类失败', e);
121
+          }
122
+        },
49 123
     // 从PHP系统加载小说目录
50
-    async loadNovels() {
51
-      const res = await this.$http.get('/php-api/novel/list');
52
-      this.novelList = res.data;
124
+    // async loadNovels() {
125
+    //   const res = await this.$http.get('/php-api/novel/list');
126
+    //   this.novelList = res.data;
127
+    // },
128
+        async loadHotNovels() {
129
+          try {
130
+            // 获取热门小说
131
+            const res = await this.$http.get('/novel/hot');
132
+            this.hotNovels = res.data;
133
+          } catch (e) {
134
+            console.error('加载热门小说失败', e);
135
+          }
136
+        },
137
+    async loadNovels(categoryId = 0) {
138
+      try {
139
+        const url = categoryId 
140
+          ? `/novel/list?categoryId=${categoryId}`
141
+          : '/novel/list';
142
+          
143
+        const res = await this.$http.get(url);
144
+        this.novels = res.data;
145
+        
146
+        // 更新当前分类名称
147
+        if (categoryId === 0) {
148
+          this.currentCategoryName = '全部';
149
+        } else {
150
+          const category = this.categories.find(c => c.id === categoryId);
151
+          this.currentCategoryName = category ? category.name : '';
152
+        }
153
+      } catch (e) {
154
+        uni.showToast({ title: '加载小说失败', icon: 'none' });
155
+      }
156
+    },
157
+    changeCategory(categoryId) {
158
+      this.activeCategory = categoryId;
159
+      this.loadNovels(categoryId);
160
+    },
161
+    openNovel(novel) {
162
+      uni.navigateTo({
163
+        url: `/pages/novel/detail?id=${novel.id}`
164
+      });
165
+    },
166
+    applyAuthor() {
167
+      uni.navigateTo({
168
+        url: '/pages/author/apply'
169
+      });
53 170
     },
54
-    
55 171
     // 打开小说详情页
56 172
     openNovel(novel) {
57 173
       uni.navigateTo({
@@ -62,7 +178,133 @@ export default {
62 178
 }
63 179
 </script>
64 180
 <style scoped>
181
+.become-author {
182
+  position: fixed;
183
+  bottom: 120rpx; /* 在TabBar上方 */
184
+  left: 50%;
185
+  transform: translateX(-50%);
186
+  background-color: #3cc51f;
187
+  color: white;
188
+  padding: 16rpx 40rpx;
189
+  border-radius: 50rpx;
190
+  font-size: 28rpx;
191
+  box-shadow: 0 4rpx 12rpx rgba(60, 197, 31, 0.3);
192
+  z-index: 999;
193
+}
65 194
 .novel-list-page {
66
-  padding-bottom: 120rpx !important;
195
+  padding: 20rpx;
196
+  padding-bottom: 40rpx;
197
+}
198
+
199
+.category-nav {
200
+  white-space: nowrap;
201
+  margin-bottom: 30rpx;
202
+}
203
+
204
+.category-item {
205
+  display: inline-block;
206
+  padding: 10rpx 30rpx;
207
+  margin-right: 20rpx;
208
+  border-radius: 50rpx;
209
+  background-color: #f5f5f5;
210
+  font-size: 28rpx;
211
+}
212
+
213
+.category-item.active {
214
+  background-color: #3cc51f;
215
+  color: white;
216
+}
217
+
218
+.section {
219
+  margin-bottom: 40rpx;
220
+}
221
+
222
+.section-header {
223
+  display: flex;
224
+  justify-content: space-between;
225
+  align-items: center;
226
+  margin-bottom: 20rpx;
227
+}
228
+
229
+.section-title {
230
+  font-size: 32rpx;
231
+  font-weight: bold;
232
+}
233
+
234
+.section-more {
235
+  font-size: 24rpx;
236
+  color: #888;
237
+}
238
+
239
+.hot-list {
240
+  white-space: nowrap;
241
+}
242
+
243
+.hot-item {
244
+  display: inline-block;
245
+  width: 200rpx;
246
+  margin-right: 20rpx;
247
+}
248
+
249
+.hot-cover {
250
+  width: 200rpx;
251
+  height: 280rpx;
252
+  border-radius: 8rpx;
253
+}
254
+
255
+.hot-title {
256
+  display: block;
257
+  font-size: 26rpx;
258
+  margin-top: 10rpx;
259
+  white-space: nowrap;
260
+  overflow: hidden;
261
+  text-overflow: ellipsis;
262
+}
263
+
264
+.novel-grid {
265
+  display: grid;
266
+  grid-template-columns: repeat(3, 1fr);
267
+  gap: 20rpx;
268
+}
269
+
270
+.novel-item {
271
+  text-align: center;
272
+}
273
+
274
+.novel-cover {
275
+  width: 100%;
276
+  height: 300rpx;
277
+  border-radius: 8rpx;
278
+}
279
+
280
+.novel-title {
281
+  display: block;
282
+  font-size: 26rpx;
283
+  margin-top: 10rpx;
284
+  overflow: hidden;
285
+  text-overflow: ellipsis;
286
+  display: -webkit-box;
287
+  -webkit-line-clamp: 1;
288
+  -webkit-box-orient: vertical;
289
+}
290
+
291
+.novel-author {
292
+  display: block;
293
+  font-size: 22rpx;
294
+  color: #888;
295
+}
296
+
297
+.become-author {
298
+  position: fixed;
299
+  bottom: 120rpx; /* 在TabBar上方 */
300
+  left: 50%;
301
+  transform: translateX(-50%);
302
+  background-color: #3cc51f;
303
+  color: white;
304
+  padding: 16rpx 40rpx;
305
+  border-radius: 50rpx;
306
+  font-size: 28rpx;
307
+  box-shadow: 0 4rpx 12rpx rgba(60, 197, 31, 0.3);
308
+  z-index: 999;
67 309
 }
68 310
 </style>

+ 155
- 0
RuoYi-App/pages/novel/read.vue Voir le fichier

@@ -0,0 +1,155 @@
1
+<template>
2
+  <view class="reading-container">
3
+    <!-- 顶部章节信息 -->
4
+    <view class="chapter-header">
5
+      <text class="chapter-title">{{ chapter.title }}</text>
6
+    </view>
7
+    
8
+    <!-- 小说内容 -->
9
+    <scroll-view scroll-y class="content-container">
10
+      <rich-text :nodes="chapter.content" class="content-text"></rich-text>
11
+    </scroll-view>
12
+    
13
+    <!-- 底部操作栏 -->
14
+    <view class="action-bar">
15
+      <button @click="prevChapter">上一章</button>
16
+      <button @click="showCatalog">目录</button>
17
+      <button @click="nextChapter">下一章</button>
18
+    </view>
19
+    
20
+    <!-- 章节间广告 -->
21
+    <ad-banner v-if="showAd" :ads="midAds" />
22
+  </view>
23
+</template>
24
+
25
+<script>
26
+import AdBanner from '@/components/AdBanner';
27
+
28
+export default {
29
+  components: { AdBanner },
30
+  data() {
31
+    return {
32
+      novelId: null,
33
+      chapterId: null,
34
+      chapter: {},
35
+      chapters: [],
36
+      showAd: false,
37
+      midAds: []
38
+    };
39
+  },
40
+  onLoad(options) {
41
+    this.novelId = options.novelId;
42
+    this.chapterId = options.chapterId;
43
+    this.loadChapterData();
44
+    this.scheduleAd();
45
+  },
46
+  methods: {
47
+    async loadChapterData() {
48
+      try {
49
+        // 加载章节列表
50
+        const chaptersRes = await this.$http.get(`/novel/${this.novelId}/chapters`);
51
+        this.chapters = chaptersRes.data;
52
+        
53
+        // 加载当前章节内容
54
+        await this.loadChapterContent(this.chapterId || this.chapters[0].id);
55
+      } catch (e) {
56
+        uni.showToast({ title: '加载失败', icon: 'none' });
57
+      }
58
+    },
59
+    async loadChapterContent(chapterId) {
60
+      const res = await this.$http.get(`/novel/chapter/${chapterId}`);
61
+      this.chapter = res.data;
62
+      this.chapterId = chapterId;
63
+      
64
+      // 保存阅读进度
65
+      this.saveReadingProgress();
66
+    },
67
+    saveReadingProgress() {
68
+      const progress = {
69
+        novelId: this.novelId,
70
+        chapterId: this.chapterId,
71
+        timestamp: Date.now()
72
+      };
73
+      uni.setStorageSync('readingProgress', progress);
74
+      
75
+      // 同步到后台
76
+      if (this.$store.getters.token) {
77
+        this.$http.post('/user/reading-progress', progress);
78
+      }
79
+    },
80
+    prevChapter() {
81
+      const currentIndex = this.chapters.findIndex(c => c.id === this.chapterId);
82
+      if (currentIndex > 0) {
83
+        this.loadChapterContent(this.chapters[currentIndex - 1].id);
84
+      }
85
+    },
86
+    nextChapter() {
87
+      const currentIndex = this.chapters.findIndex(c => c.id === this.chapterId);
88
+      if (currentIndex < this.chapters.length - 1) {
89
+        this.loadChapterContent(this.chapters[currentIndex + 1].id);
90
+      }
91
+    },
92
+    showCatalog() {
93
+      uni.navigateTo({
94
+        url: `/pages/novel/catalog?novelId=${this.novelId}`
95
+      });
96
+    },
97
+    async scheduleAd() {
98
+      // 30秒后显示广告
99
+      setTimeout(async () => {
100
+        const res = await this.$http.get('/ad/position?code=CHAPTER_MID');
101
+        this.midAds = res.data;
102
+        this.showAd = true;
103
+        
104
+        // 10秒后隐藏
105
+        setTimeout(() => this.showAd = false, 10000);
106
+      }, 30000);
107
+    }
108
+  }
109
+}
110
+</script>
111
+
112
+<style scoped>
113
+.reading-container {
114
+  padding: 20rpx;
115
+  padding-bottom: 120rpx; /* 为操作栏留空间 */
116
+}
117
+
118
+.chapter-header {
119
+  padding: 20rpx 0;
120
+  border-bottom: 1rpx solid #eee;
121
+}
122
+
123
+.chapter-title {
124
+  font-size: 36rpx;
125
+  font-weight: bold;
126
+}
127
+
128
+.content-container {
129
+  height: calc(100vh - 300rpx);
130
+  padding: 30rpx 0;
131
+}
132
+
133
+.content-text {
134
+  font-size: 32rpx;
135
+  line-height: 1.8;
136
+}
137
+
138
+.action-bar {
139
+  position: fixed;
140
+  bottom: 0;
141
+  left: 0;
142
+  right: 0;
143
+  display: flex;
144
+  background: white;
145
+  padding: 20rpx;
146
+  box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.1);
147
+  z-index: 999;
148
+}
149
+
150
+.action-bar button {
151
+  flex: 1;
152
+  margin: 0 10rpx;
153
+  font-size: 28rpx;
154
+}
155
+</style>

+ 80
- 0
RuoYi-App/services/api.js Voir le fichier

@@ -1,6 +1,86 @@
1 1
 import request from '@/utils/request'
2
+// Java 后台接口
3
+const javaApi = {
4
+  // 小说相关接口
5
+  getNovelList: (params) => request({ url: '/java-api/novel/list', params }),
6
+  getChapterContent: (chapterId) => request({ url: `/java-api/novel/chapter/${chapterId}` }),
7
+  
8
+  // 作家申请
9
+  applyAuthor: (data) => request({ url: '/java-api/author/apply', method: 'post', data })
10
+}
11
+
12
+// PHP 后台接口 (预留)
13
+const phpApi = {
14
+  getNovelList: (params) => request({ url: '/php-api/novel/list', params }),
15
+  getChapterContent: (chapterId) => request({ url: `/php-api/novel/chapter/${chapterId}` })
16
+}
17
+
18
+// 根据配置切换数据源
19
+const usePhpApi = false; // 默认使用Java后台
2 20
 
3 21
 export default {
22
+	// 分类相关
23
+	  getCategories() {
24
+	    return request({ url: '/api/novel/categories' })
25
+	  },
26
+	    // 小说相关
27
+	    getHotNovels() {
28
+	      return request({ url: '/api/novel/hot' })
29
+	    },
30
+		  getNovelsByCategory(categoryId = 0) {
31
+		    return request({ 
32
+		      url: '/api/novel/list', 
33
+		      params: { categoryId }
34
+		    })
35
+		  },
36
+		    getChapters(novelId) {
37
+		      return request({ url: `/api/novel/${novelId}/chapters` })
38
+		    },
39
+		    
40
+		    getChapterContent(chapterId) {
41
+		      return request({ url: `/api/novel/chapter/${chapterId}` })
42
+		    },
43
+  // 作家申请
44
+  applyAuthor(data) {
45
+    return request({
46
+      url: '/api/author/apply',
47
+      method: 'post',
48
+      data
49
+    })
50
+  },
51
+  
52
+  // 上传相关(需要管理员权限)
53
+  uploadNovel(file, title, authorId) {
54
+    const formData = new FormData();
55
+    formData.append('file', file);
56
+    formData.append('title', title);
57
+    formData.append('authorId', authorId);
58
+    
59
+    return request({
60
+      url: '/admin/novel/upload',
61
+      method: 'post',
62
+      data: formData,
63
+      headers: { 'Content-Type': 'multipart/form-data' }
64
+    })
65
+  },
66
+    uploadChapter(file, novelId, chapterOrder) {
67
+      const formData = new FormData();
68
+      formData.append('file', file);
69
+      formData.append('novelId', novelId);
70
+      if (chapterOrder) formData.append('chapterOrder', chapterOrder);
71
+      
72
+      return request({
73
+        url: '/admin/novel/chapter/upload',
74
+        method: 'post',
75
+        data: formData,
76
+        headers: { 'Content-Type': 'multipart/form-data' }
77
+      })
78
+    }
79
+	getNovelList: usePhpApi ? phpApi.getNovelList : javaApi.getNovelList,
80
+	  getChapterContent: usePhpApi ? phpApi.getChapterContent : javaApi.getChapterContent,
81
+	  
82
+	  // 其他接口...
83
+	  applyAuthor: javaApi.applyAuthor,
4 84
   // 保存阅读进度
5 85
   saveReadingProgress(chapter) {
6 86
     return request({

+ 14
- 0
RuoYi-Vue/ruoyi-system/pom.xml Voir le fichier

@@ -82,6 +82,20 @@
82 82
             <groupId>org.aspectj</groupId>
83 83
             <artifactId>aspectjweaver</artifactId>
84 84
         </dependency>
85
+        <dependency>
86
+            <groupId>org.springframework.boot</groupId>
87
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
88
+        </dependency>
89
+        <dependency>
90
+            <groupId>org.springframework.boot</groupId>
91
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
92
+        </dependency>
93
+        <dependency>
94
+            <groupId>org.testng</groupId>
95
+            <artifactId>testng</artifactId>
96
+            <version>RELEASE</version>
97
+            <scope>compile</scope>
98
+        </dependency>
85 99
     </dependencies>
86 100
 
87 101
 </project>

+ 74
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/config/SecurityConfig.java Voir le fichier

@@ -0,0 +1,74 @@
1
+package com.ruoyi.novel.config;
2
+
3
+import com.ruoyi.novel.utils.JwtTokenProvider;
4
+import org.springframework.beans.factory.annotation.Autowired;
5
+import org.springframework.context.annotation.Bean;
6
+import org.springframework.context.annotation.Configuration;
7
+import org.springframework.security.authentication.AuthenticationManager;
8
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
9
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
10
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
11
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
12
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
13
+import org.springframework.security.config.http.SessionCreationPolicy;
14
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
15
+import org.springframework.security.crypto.password.PasswordEncoder;
16
+import org.springframework.web.cors.CorsConfiguration;
17
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
18
+import org.springframework.web.filter.CorsFilter;
19
+
20
+@Configuration
21
+@EnableWebSecurity
22
+@EnableGlobalMethodSecurity(prePostEnabled = true)
23
+public class SecurityConfig extends WebSecurityConfigurerAdapter {
24
+
25
+    @Autowired
26
+    private JwtTokenProvider tokenProvider;
27
+
28
+    @Autowired
29
+    private CustomUserDetailsService userDetailsService;
30
+
31
+    @Override
32
+    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
33
+        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
34
+    }
35
+
36
+    @Bean
37
+    public PasswordEncoder passwordEncoder() {
38
+        return new BCryptPasswordEncoder();
39
+    }
40
+
41
+    @Bean
42
+    @Override
43
+    public AuthenticationManager authenticationManagerBean() throws Exception {
44
+        return super.authenticationManagerBean();
45
+    }
46
+
47
+    @Override
48
+    protected void configure(HttpSecurity http) throws Exception {
49
+        http
50
+                .cors().and()
51
+                .csrf().disable()
52
+                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
53
+                .and()
54
+                .authorizeRequests()
55
+                .antMatchers("/api/auth/**").permitAll()
56
+                .antMatchers("/api/novel/**").permitAll() // 公开访问小说数据
57
+                .antMatchers("/admin/**").hasRole("ADMIN") // 管理员权限
58
+                .anyRequest().authenticated()
59
+                .and()
60
+                .apply(new JwtConfigurer(tokenProvider));
61
+    }
62
+
63
+    @Bean
64
+    public CorsFilter corsFilter() {
65
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
66
+        CorsConfiguration config = new CorsConfiguration();
67
+        config.setAllowCredentials(true);
68
+        config.addAllowedOrigin("*");
69
+        config.addAllowedHeader("*");
70
+        config.addAllowedMethod("*");
71
+        source.registerCorsConfiguration("/**", config);
72
+        return new CorsFilter(source);
73
+    }
74
+}

+ 76
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/controller/AdminNovelController.java Voir le fichier

@@ -0,0 +1,76 @@
1
+package com.ruoyi.novel.controller;
2
+
3
+import com.ruoyi.common.core.controller.BaseController;
4
+import com.ruoyi.common.core.domain.AjaxResult;
5
+import com.ruoyi.common.core.page.TableDataInfo;
6
+import com.ruoyi.novel.domain.AuthorApplication;
7
+import com.ruoyi.novel.domain.Chapter;
8
+import com.ruoyi.novel.domain.Novel;
9
+import com.ruoyi.novel.service.NovelService;
10
+import org.springframework.beans.factory.annotation.Autowired;
11
+import org.springframework.http.HttpStatus;
12
+import org.springframework.http.ResponseEntity;
13
+import org.springframework.web.bind.annotation.*;
14
+import org.springframework.web.multipart.MultipartFile;
15
+
16
+import java.util.List;
17
+import java.util.Map;
18
+
19
+@RestController
20
+@RequestMapping("/admin/novel")
21
+public class AdminNovelController extends BaseController {
22
+
23
+    @Autowired
24
+    private NovelService novelService;
25
+
26
+    // 上传整本小说
27
+    // 上传整本小说
28
+    @PostMapping("/upload")
29
+    public AjaxResult uploadNovel(@RequestParam("file") MultipartFile file,
30
+                                  @RequestParam String title,
31
+                                  @RequestParam Long authorId) {
32
+        try {
33
+            Novel novel = novelService.processNovelUpload(file, title, authorId);
34
+            return AjaxResult.success(novel);
35
+        } catch (Exception e) {
36
+            return AjaxResult.error(e.getMessage());
37
+        }
38
+    }
39
+
40
+    // 按章节上传
41
+    // 上传章节
42
+    @PostMapping("/chapter/upload")
43
+    public AjaxResult uploadChapter(@RequestParam("file") MultipartFile file,
44
+                                    @RequestParam Long novelId,
45
+                                    @RequestParam(required = false) Integer chapterOrder) {
46
+        try {
47
+            Chapter chapter = novelService.processChapterUpload(file, novelId, chapterOrder);
48
+            return AjaxResult.success(chapter);
49
+        } catch (Exception e) {
50
+            return AjaxResult.error(e.getMessage());
51
+        }
52
+    }
53
+
54
+    // 管理作家申请
55
+    // 获取作家申请列表
56
+    @GetMapping("/author-applications")
57
+    public TableDataInfo getAuthorApplications(@RequestParam(defaultValue = "0") Integer status) {
58
+        startPage();
59
+        List<AuthorApplication> list = novelService.getAuthorApplications(status);
60
+        return getDataTable(list);
61
+    }
62
+
63
+    // 批准作家申请
64
+    @PostMapping("/author-application/{id}/approve")
65
+    public AjaxResult approveAuthorApplication(@PathVariable Long id) {
66
+        novelService.approveAuthorApplication(id);
67
+        return AjaxResult.success("申请已批准");
68
+    }
69
+
70
+    // 拒绝作家申请
71
+    @PostMapping("/author-application/{id}/reject")
72
+    public AjaxResult rejectAuthorApplication(@PathVariable Long id) {
73
+        novelService.rejectAuthorApplication(id);
74
+        return AjaxResult.success("申请已拒绝");
75
+    }
76
+}

+ 89
- 3
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/controller/NovelController.java Voir le fichier

@@ -3,30 +3,116 @@ package com.ruoyi.novel.controller;
3 3
 import com.github.pagehelper.PageHelper;
4 4
 import com.github.pagehelper.PageInfo;
5 5
 import com.ruoyi.common.constant.HttpStatus;
6
+import com.ruoyi.common.core.controller.BaseController;
6 7
 import com.ruoyi.common.core.domain.AjaxResult;
7 8
 import com.ruoyi.common.core.page.PageDomain;
8 9
 import com.ruoyi.common.core.page.TableDataInfo;
9 10
 import com.ruoyi.common.core.page.TableSupport;
11
+import com.ruoyi.novel.domain.AuthorApplicationDTO;
10 12
 import com.ruoyi.novel.domain.Novel;
11 13
 import com.ruoyi.novel.mapper.NovelMapper;
12 14
 import com.ruoyi.novel.service.NovelSearchService;
13 15
 import com.ruoyi.novel.service.NovelService;
14 16
 import org.springframework.beans.factory.annotation.Autowired;
17
+import org.springframework.http.ResponseEntity;
15 18
 import org.springframework.web.bind.annotation.*;
16 19
 
17 20
 import java.util.List;
21
+import java.util.Map;
18 22
 
19 23
 // NovelController.java
20 24
 @RestController
21
-@RequestMapping("/novel")
22
-public class NovelController {
25
+@RequestMapping("/api/novel")
26
+public class NovelController extends BaseController {
23 27
 
24 28
     @Autowired
25 29
     private NovelService novelService;
30
+
31
+    @Autowired
32
+    private IAuthService authService;
26 33
     @Autowired
27 34
     private NovelSearchService searchService;
28 35
     @Autowired
29 36
     private NovelMapper novelMapper;
37
+
38
+    // 获取小说分类
39
+//    @GetMapping("/novel/categories")
40
+//    public ResponseEntity<?> getCategories() {
41
+//        return ResponseEntity.ok(novelService.getAllCategories());
42
+//    }
43
+    // 获取分类列表
44
+    @GetMapping("/categories")
45
+    public AjaxResult getCategories() {
46
+        return AjaxResult.success(novelService.getAllCategories());
47
+    }
48
+//    @GetMapping("/novel/categories")
49
+//    public ResponseEntity<?> getCategories() {
50
+//        return ResponseEntity.ok(novelService.getAllCategories());
51
+//    }
52
+    // 获取热门小说
53
+// 获取热门小说
54
+@GetMapping("/hot")
55
+public TableDataInfo getHotNovels() {
56
+    List<Novel> list = novelService.getHotNovels();
57
+    return getDataTable(list);
58
+}
59
+//    @GetMapping("/novel/hot")
60
+//    public ResponseEntity<?> getHotNovels() {
61
+//        return ResponseEntity.ok(novelService.getHotNovels());
62
+//    }
63
+    // 按分类获取小说
64
+// 按分类获取小说
65
+@GetMapping("/list")
66
+public TableDataInfo getNovelsByCategory(@RequestParam(required = false) Long categoryId) {
67
+    return novelService.getNovelsByCategory(categoryId);
68
+}
69
+//    @GetMapping("/novel/list")
70
+//    public ResponseEntity<?> getNovelsByCategory(@RequestParam(required = false) Integer categoryId) {
71
+//        return ResponseEntity.ok(novelService.getNovelsByCategory(categoryId));
72
+//    }
73
+    // 获取小说章节列表
74
+// 获取小说章节列表
75
+@GetMapping("/{novelId}/chapters")
76
+public AjaxResult getChapters(@PathVariable Long novelId) {
77
+    return AjaxResult.success(novelService.getChaptersByNovelId(novelId));
78
+}
79
+
80
+//    @GetMapping("/novel/{novelId}/chapters")
81
+//    public ResponseEntity<?> getChapters(@PathVariable Long novelId) {
82
+//        return ResponseEntity.ok(novelService.getChaptersByNovelId(novelId));
83
+//    }
84
+    // 获取章节内容
85
+// 获取章节内容
86
+@GetMapping("/chapter/{chapterId}")
87
+public AjaxResult getChapterContent(@PathVariable Long chapterId) {
88
+    return AjaxResult.success(novelService.getChapterContent(chapterId));
89
+}
90
+//    @GetMapping("/novel/chapter/{chapterId}")
91
+//    public ResponseEntity<?> getChapterContent(@PathVariable Long chapterId) {
92
+//        return ResponseEntity.ok(novelService.getChapterContent(chapterId));
93
+//    }
94
+    // 提交作家申请
95
+//    @PostMapping("/author/apply")
96
+//    public ResponseEntity<?> applyAuthor(@RequestBody AuthorApplicationDTO dto,
97
+//                                         @RequestHeader("Authorization") String token) {
98
+//        Long userId = authService.getUserIdFromToken(token);
99
+//        novelService.submitAuthorApplication(dto, userId);
100
+//        return ResponseEntity.ok("申请已提交");
101
+//    }
102
+
103
+
104
+    // 提交作家申请
105
+    @PostMapping("/author/apply")
106
+    public AjaxResult applyAuthor(@RequestBody AuthorApplicationDTO dto,
107
+                                  @RequestHeader("Authorization") String token) {
108
+        try {
109
+            Long userId = authService.getUserIdFromToken(token);
110
+            novelService.submitAuthorApplication(dto, userId);
111
+            return AjaxResult.success("申请已提交");
112
+        } catch (Exception e) {
113
+            return AjaxResult.error(e.getMessage());
114
+        }
115
+    }
30 116
     @GetMapping("/list")
31 117
     public TableDataInfo list(Novel novel) {
32 118
         startPage();
@@ -42,7 +128,7 @@ public class NovelController {
42 128
         return toAjax(novelService.deleteNovelByIds(ids));
43 129
     }
44 130
 
45
-    private AjaxResult toAjax(int rows) {
131
+    public AjaxResult toAjax(int rows) {
46 132
         return rows > 0 ? AjaxResult.success() : AjaxResult.error();
47 133
     }
48 134
     // 若依框架的分页方法

+ 36
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/AuthorApplication.java Voir le fichier

@@ -0,0 +1,36 @@
1
+package com.ruoyi.novel.domain;
2
+
3
+import lombok.Data;
4
+
5
+import javax.persistence.*;
6
+import java.util.Date;
7
+
8
+// 作家申请实体
9
+@Data
10
+@Entity
11
+@Table(name = "author_application")
12
+public class AuthorApplication {
13
+    @Id
14
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
15
+    private Long id;
16
+    @Column(name = "user_id")
17
+    private Long userId;
18
+    @Column(name = "real_name")
19
+    private String realName;
20
+    private String contact;
21
+    @Column(name = "novel_type")
22
+    private String novelType;
23
+    @Column(name = "novel_title")
24
+    private String novelTitle;
25
+    @Column(name = "novel_description")
26
+    private String novelDescription;
27
+    @Lob
28
+    @Column(columnDefinition = "TEXT")
29
+    private String sampleContent;
30
+
31
+    private Integer status = 0; // 0: 待审核, 1: 通过, 2: 拒绝
32
+    @Column(name = "create_time")
33
+    private Date createTime;
34
+
35
+    // Getters and setters
36
+}

+ 30
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/AuthorApplicationDTO.java Voir le fichier

@@ -0,0 +1,30 @@
1
+package com.ruoyi.novel.domain;
2
+
3
+// AuthorApplicationDTO.java
4
+public class AuthorApplicationDTO {
5
+    private String realName;
6
+    private String contact;
7
+    private String novelType;
8
+    private String novelTitle;
9
+    private String description;
10
+    private String sampleContent;
11
+
12
+    // Getters and setters
13
+    public String getRealName() { return realName; }
14
+    public void setRealName(String realName) { this.realName = realName; }
15
+
16
+    public String getContact() { return contact; }
17
+    public void setContact(String contact) { this.contact = contact; }
18
+
19
+    public String getNovelType() { return novelType; }
20
+    public void setNovelType(String novelType) { this.novelType = novelType; }
21
+
22
+    public String getNovelTitle() { return novelTitle; }
23
+    public void setNovelTitle(String novelTitle) { this.novelTitle = novelTitle; }
24
+
25
+    public String getDescription() { return description; }
26
+    public void setDescription(String description) { this.description = description; }
27
+
28
+    public String getSampleContent() { return sampleContent; }
29
+    public void setSampleContent(String sampleContent) { this.sampleContent = sampleContent; }
30
+}

+ 31
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/Chapter.java Voir le fichier

@@ -0,0 +1,31 @@
1
+package com.ruoyi.novel.domain;
2
+
3
+
4
+import lombok.Data;
5
+
6
+import javax.persistence.*;
7
+import java.util.Date;
8
+
9
+@Data
10
+@Entity
11
+@Table(name = "chapter")
12
+public class Chapter {
13
+    @Id
14
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
15
+    private Long id;
16
+    @Column(name = "novel_id")
17
+    private Long novelId;
18
+    private String title;
19
+    @Lob
20
+    @Column(columnDefinition = "TEXT")
21
+    private String content;
22
+    @Column(name = "chapter_order")
23
+    private Integer chapterOrder;
24
+
25
+    @Column(name = "create_time")
26
+    private Date createTime;
27
+
28
+
29
+
30
+    // Getters and setters
31
+}

+ 9
- 3
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/Novel.java Voir le fichier

@@ -9,6 +9,7 @@ import org.springframework.data.elasticsearch.annotations.Document;
9 9
 import org.springframework.data.elasticsearch.annotations.Field;
10 10
 import org.springframework.data.elasticsearch.annotations.FieldType;
11 11
 
12
+import javax.persistence.Column;
12 13
 import java.util.Date;
13 14
 
14 15
 // Novel.java
@@ -17,23 +18,28 @@ import java.util.Date;
17 18
 @Document(indexName = "novels")
18 19
 public class Novel {
19 20
     @Id
21
+    //@GeneratedValue(strategy = GenerationType.IDENTITY)
20 22
     @TableId(type = IdType.AUTO)
21 23
     private Long id;
22 24
     @Field(type = FieldType.Text, analyzer = "ik_max_word")
23 25
     private String title;
24 26
     @Field(type = FieldType.Keyword)
25 27
     private String author;
28
+    private Long authorId;
29
+    private String cover;
26 30
     private String coverImg;
27 31
     @Field(type = FieldType.Long)
28 32
     private Long categoryId;
29 33
     @Field(type = FieldType.Keyword)
30
-    private String status;// 连载/完本
34
+    private Integer status = 0; // 0: 连载中, 1: 已完结// 连载/完本
31 35
     @Field(type = FieldType.Text, analyzer = "ik_smart")
32 36
     private String description;
33 37
     private Long wordCount;
34 38
     @Field(type = FieldType.Long)
35 39
     private Long readCount;
36 40
     @Field(type = FieldType.Date)
37
-    private Date createTime;
38
-    private Date updateTime;
41
+    @Column(name = "create_time")
42
+    private Date createTime = new Date();
43
+    @Column(name = "update_time")
44
+    private Date updateTime = new Date();
39 45
 }

+ 1
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/NovelChapter.java Voir le fichier

@@ -14,6 +14,7 @@ public class NovelChapter {
14 14
     @TableId(type = IdType.AUTO)
15 15
     private Long id;
16 16
     private Long novelId;
17
+    private String title;
17 18
     private String chapterTitle;
18 19
     private Integer chapterOrder;
19 20
     private Date publishTime;

+ 14
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/AuthorApplicationMapper.java Voir le fichier

@@ -0,0 +1,14 @@
1
+package com.ruoyi.novel.mapper;
2
+
3
+import com.ruoyi.novel.domain.AuthorApplication;
4
+
5
+import java.util.List;
6
+
7
+// AuthorApplicationMapper.java
8
+public interface AuthorApplicationMapper {
9
+    AuthorApplication selectAuthorApplicationById(Long id);
10
+    AuthorApplication selectPendingApplication(Long userId);
11
+    int insertAuthorApplication(AuthorApplication application);
12
+    int updateAuthorApplication(AuthorApplication application);
13
+    List<AuthorApplication> selectAuthorApplicationList(AuthorApplication application);
14
+}

+ 10
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/CategoryMapper.java Voir le fichier

@@ -0,0 +1,10 @@
1
+package com.ruoyi.novel.mapper;
2
+
3
+import jdk.jfr.Category;
4
+
5
+import java.util.List;
6
+
7
+// CategoryMapper.java
8
+public interface CategoryMapper {
9
+    List<Category> selectCategoryList(Category category);
10
+}

+ 9
- 1
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/ChapterMapper.java Voir le fichier

@@ -1,4 +1,12 @@
1 1
 package com.ruoyi.novel.mapper;
2 2
 
3
-public class ChapterMapper {
3
+import com.ruoyi.novel.domain.Chapter;
4
+
5
+import java.util.List;
6
+
7
+public interface ChapterMapper {
8
+    Chapter selectChapterById(Long id);
9
+    int insertChapter(Chapter chapter);
10
+    List<Chapter> selectChapterList(Chapter chapter);
11
+    Integer selectMaxChapterOrder(Long novelId);
4 12
 }

+ 9
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/NovelMapper.java Voir le fichier

@@ -7,6 +7,7 @@ import org.apache.ibatis.annotations.Select;
7 7
 import org.apache.ibatis.annotations.Update;
8 8
 
9 9
 import java.util.List;
10
+import java.util.Map;
10 11
 
11 12
 // NovelMapper.java
12 13
 public interface NovelMapper extends BaseMapper<Novel> {
@@ -28,5 +29,13 @@ public interface NovelMapper extends BaseMapper<Novel> {
28 29
     // 自定义方法:更新阅读量
29 30
     @Update("UPDATE novel SET read_count = read_count + #{increment} WHERE id = #{id}")
30 31
     int incrementReadCount(@Param("id") Long id, @Param("increment") Long increment);
32
+
33
+    //Novel selectNovelById(Long id);
34
+   // int insertNovel(Novel novel);
35
+    //int updateNovel(Novel novel);
36
+  //  List<Novel> selectNovelList(Novel novel);
37
+    List<Novel> selectHotNovels(Map<String, Object> params);
38
+
39
+
31 40
 }
32 41
 

+ 15
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/AuthorApplicationRepository.java Voir le fichier

@@ -0,0 +1,15 @@
1
+package com.ruoyi.novel.service;
2
+
3
+import com.ruoyi.novel.domain.AuthorApplication;
4
+import org.springframework.data.jpa.repository.JpaRepository;
5
+
6
+import java.util.List;
7
+import java.util.Optional;
8
+
9
+// AuthorApplicationRepository.java
10
+public interface AuthorApplicationRepository extends JpaRepository<AuthorApplication, Long> {
11
+
12
+    List<AuthorApplication> findByStatus(Integer status);
13
+
14
+    Optional<AuthorApplication> findByUserIdAndStatus(Long userId, Integer status);
15
+}

+ 9
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/CategoryRepository.java Voir le fichier

@@ -0,0 +1,9 @@
1
+package com.ruoyi.novel.service;
2
+
3
+import jdk.jfr.Category;
4
+import org.springframework.data.jpa.repository.JpaRepository;
5
+
6
+// CategoryRepository.java
7
+public interface CategoryRepository extends JpaRepository<Category, Integer> {
8
+    // 基础方法已由JpaRepository提供
9
+}

+ 17
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/ChapterRepository.java Voir le fichier

@@ -0,0 +1,17 @@
1
+package com.ruoyi.novel.service;
2
+
3
+import com.ruoyi.novel.domain.Chapter;
4
+import org.apache.ibatis.annotations.Param;
5
+import org.springframework.data.elasticsearch.annotations.Query;
6
+import org.springframework.data.jpa.repository.JpaRepository;
7
+
8
+import java.util.List;
9
+
10
+// ChapterRepository.java
11
+public interface ChapterRepository extends JpaRepository<Chapter, Long> {
12
+
13
+    List<Chapter> findByNovelIdOrderByChapterOrderAsc(Long novelId);
14
+
15
+    @Query("SELECT MAX(c.chapterOrder) FROM Chapter c WHERE c.novelId = :novelId")
16
+    Integer findMaxChapterOrderByNovelId(@Param("novelId") Long novelId);
17
+}

+ 19
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/NovelRepository.java Voir le fichier

@@ -0,0 +1,19 @@
1
+package com.ruoyi.novel.service;
2
+
3
+import com.ruoyi.novel.domain.Novel;
4
+import org.apache.ibatis.annotations.Param;
5
+import org.springframework.data.elasticsearch.annotations.Query;
6
+import org.springframework.data.jpa.repository.JpaRepository;
7
+
8
+import java.awt.print.Pageable;
9
+import java.util.Date;
10
+import java.util.List;
11
+
12
+// NovelRepository.java
13
+public interface NovelRepository extends JpaRepository<Novel, Long> {
14
+
15
+    @Query("SELECT n FROM Novel n WHERE n.updateTime > :since ORDER BY n.readCount DESC")
16
+    List<Novel> findHotNovels(@Param("since") Date since, Pageable pageable);
17
+
18
+    List<Novel> findByCategoryId(Integer categoryId, Pageable pageable);
19
+}

+ 12
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/UserRepository.java Voir le fichier

@@ -0,0 +1,12 @@
1
+package com.ruoyi.novel.service;
2
+
3
+import org.elasticsearch.client.security.user.User;
4
+import org.springframework.data.jpa.repository.JpaRepository;
5
+
6
+import java.util.Optional;
7
+
8
+// UserRepository.java
9
+public interface UserRepository extends JpaRepository<User, Long> {
10
+
11
+    Optional<User> findByUsername(String username);
12
+}

+ 42
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/AuthServiceImpl.java Voir le fichier

@@ -0,0 +1,42 @@
1
+package com.ruoyi.novel.service.impl;
2
+
3
+import com.ruoyi.common.core.domain.model.LoginUser;
4
+import com.ruoyi.novel.service.UserRepository;
5
+import com.ruoyi.novel.utils.JwtTokenProvider;
6
+import org.springframework.beans.factory.annotation.Autowired;
7
+import org.springframework.security.core.token.TokenService;
8
+import org.springframework.stereotype.Service;
9
+
10
+@Service
11
+public class AuthServiceImpl implements IAuthService  {
12
+
13
+    @Autowired
14
+    private JwtTokenProvider tokenProvider;
15
+
16
+    @Autowired
17
+    private UserRepository userRepository;
18
+    @Autowired
19
+    private TokenService tokenService;
20
+
21
+    @Override
22
+    public Long getUserIdFromToken(String token) {
23
+        // 若依标准方式获取用户ID
24
+        LoginUser loginUser = tokenService.getLoginUser(token);
25
+        if (loginUser != null && loginUser.getUser() != null) {
26
+            return loginUser.getUser().getUserId();
27
+        }
28
+        throw new RuntimeException("无效的Token或用户不存在");
29
+    }
30
+    @Autowired
31
+    private TokenService tokenService;
32
+
33
+    @Override
34
+    public Long getUserIdFromToken(String token) {
35
+        // 若依标准方式获取用户ID
36
+        LoginUser loginUser = tokenService.getLoginUser(token);
37
+        if (loginUser != null && loginUser.getUser() != null) {
38
+            return loginUser.getUser().getUserId();
39
+        }
40
+        throw new RuntimeException("无效的Token或用户不存在");
41
+    }
42
+}

+ 297
- 4
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/NovelServiceImpl.java Voir le fichier

@@ -1,27 +1,320 @@
1 1
 package com.ruoyi.novel.service.impl;
2 2
 
3
+import com.ruoyi.common.constant.HttpStatus;
4
+import com.ruoyi.common.core.domain.entity.SysUser;
5
+import com.ruoyi.common.core.page.PageDomain;
6
+import com.ruoyi.common.core.page.TableDataInfo;
7
+import com.ruoyi.common.utils.PageUtils;
8
+import com.ruoyi.common.utils.StringUtils;
9
+import com.ruoyi.novel.config.TableSupport;
10
+import com.ruoyi.novel.domain.AuthorApplication;
11
+import com.ruoyi.novel.domain.AuthorApplicationDTO;
12
+import com.ruoyi.novel.domain.Chapter;
3 13
 import com.ruoyi.novel.domain.Novel;
14
+import com.ruoyi.novel.mapper.AuthorApplicationMapper;
15
+import com.ruoyi.novel.mapper.CategoryMapper;
16
+import com.ruoyi.novel.mapper.ChapterMapper;
4 17
 import com.ruoyi.novel.mapper.NovelMapper;
5
-import com.ruoyi.novel.service.NovelService;
18
+import com.ruoyi.novel.service.*;
19
+import com.ruoyi.system.mapper.SysUserMapper;
20
+import jdk.jfr.Category;
21
+import org.elasticsearch.client.security.user.User;
6 22
 import org.slf4j.Logger;
7 23
 import org.slf4j.LoggerFactory;
8 24
 import org.springframework.beans.factory.annotation.Autowired;
25
+import org.springframework.data.domain.PageRequest;
9 26
 import org.springframework.stereotype.Service;
10 27
 import org.springframework.transaction.annotation.Transactional;
28
+import org.springframework.web.multipart.MultipartFile;
11 29
 
12
-import java.util.Arrays;
13
-import java.util.Date;
14
-import java.util.List;
30
+import java.io.BufferedReader;
31
+import java.io.IOException;
32
+import java.io.InputStreamReader;
33
+import java.nio.charset.StandardCharsets;
34
+import java.util.*;
15 35
 
16 36
 // NovelServiceImpl.java
17 37
 @Service
18 38
 public class NovelServiceImpl implements NovelService {
19 39
     private static final Logger logger = LoggerFactory.getLogger(NovelServiceImpl.class);
40
+    @Autowired
41
+    private NovelRepository novelRepository;
42
+
43
+    @Autowired
44
+    private ChapterRepository chapterRepository;
45
+    @Autowired
46
+    private AuthorApplicationRepository authorApplicationRepository;
47
+
48
+    @Autowired
49
+    private CategoryRepository categoryRepository;
20 50
 
21 51
     @Autowired
52
+    private UserRepository userRepository;
53
+    @Autowired
22 54
     private NovelMapper novelMapper;
55
+    @Autowired
56
+    private ChapterMapper chapterMapper;
57
+
58
+    @Autowired
59
+    private AuthorApplicationMapper authorApplicationMapper;
60
+
61
+    @Autowired
62
+    private CategoryMapper categoryMapper;
63
+
64
+    @Autowired
65
+    private SysUserMapper userMapper;
66
+    @Override
67
+    @Transactional
68
+    public Novel processNovelUpload(MultipartFile file, String title, Long authorId) {
69
+        try {
70
+            // 验证作者
71
+            SysUser author = userMapper.selectUserById(authorId);
72
+            if (author == null) {
73
+                throw new RuntimeException("作者不存在");
74
+            }
75
+
76
+            // 创建小说记录
77
+            Novel novel = new Novel();
78
+            novel.setTitle(title);
79
+            novel.setAuthorId(authorId);
80
+            novel.setAuthor(author.getNickName());
81
+            novel.setCover("/default-cover.jpg");
82
+            novel.setDescription("新上传小说");
83
+            novel.setCategoryId(0L);
84
+            novel.setStatus(0); // 0:连载中
85
+            novel.setCreateTime(new Date());
86
+            novel.setUpdateTime(new Date());
87
+            novelMapper.insertNovel(novel);
88
+
89
+            // 解析并保存章节
90
+            List<Chapter> chapters = parseChapters(file);
91
+            for (int i = 0; i < chapters.size(); i++) {
92
+                Chapter chapter = chapters.get(i);
93
+                chapter.setNovelId(novel.getId());
94
+                chapter.setChapterOrder(i + 1);
95
+                chapter.setCreateTime(new Date());
96
+                chapterMapper.insertChapter(chapter);
97
+            }
98
+
99
+            return novel;
100
+        } catch (IOException e) {
101
+            throw new RuntimeException("处理小说上传失败: " + e.getMessage());
102
+        }
103
+    }
104
+
105
+    private List<Chapter> parseChapters(MultipartFile file) throws IOException {
106
+        List<Chapter> chapters = new ArrayList<>();
107
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) {
108
+            StringBuilder content = new StringBuilder();
109
+            String line;
110
+            String currentTitle = "第一章";
111
+            boolean isFirstChapter = true;
112
+
113
+            while ((line = reader.readLine()) != null) {
114
+                if (isChapterTitle(line)) {
115
+                    // 保存上一章
116
+                    if (content.length() > 0) {
117
+                        chapters.add(createChapter(currentTitle, content.toString()));
118
+                        content = new StringBuilder();
119
+                    }
120
+                    currentTitle = line.trim();
121
+                    isFirstChapter = false;
122
+                } else if (!isFirstChapter) {
123
+                    content.append(line).append("\n");
124
+                }
125
+            }
126
+
127
+            // 添加最后一章
128
+            if (content.length() > 0) {
129
+                chapters.add(createChapter(currentTitle, content.toString()));
130
+            }
131
+        }
132
+        return chapters;
133
+    }
134
+
135
+    private boolean isChapterTitle(String line) {
136
+        return line != null &&
137
+                (line.matches("第[零一二三四五六七八九十百千]+章\\s+.+") ||
138
+                        line.matches("第\\d+章\\s+.+"));
139
+    }
140
+
141
+    private Chapter createChapter(String title, String content) {
142
+        Chapter chapter = new Chapter();
143
+        chapter.setTitle(title);
144
+        chapter.setContent(content);
145
+        return chapter;
146
+    }
147
+    @Override
148
+    @Transactional
149
+    public Chapter processChapterUpload(MultipartFile file, Long novelId, Integer chapterOrder) {
150
+        try {
151
+            // 验证小说存在
152
+            Novel novel = novelMapper.selectNovelById(novelId);
153
+            if (novel == null) {
154
+                throw new RuntimeException("小说不存在");
155
+            }
156
+
157
+            // 确定章节顺序
158
+            if (chapterOrder == null) {
159
+                chapterOrder = chapterMapper.selectMaxChapterOrder(novelId);
160
+                if (chapterOrder == null) chapterOrder = 0;
161
+                chapterOrder += 1;
162
+            }
163
+
164
+            // 读取章节内容
165
+            String content = new String(file.getBytes());
166
+
167
+            // 创建章节
168
+            Chapter chapter = new Chapter();
169
+            chapter.setNovelId(novelId);
170
+            chapter.setTitle("第" + chapterOrder + "章");
171
+            chapter.setContent(content);
172
+            chapter.setChapterOrder(chapterOrder);
173
+            chapter.setCreateTime(new Date());
174
+            chapterMapper.insertChapter(chapter);
175
+
176
+            // 更新小说时间
177
+            novel.setUpdateTime(new Date());
178
+            novelMapper.updateNovel(novel);
179
+
180
+            return chapter;
181
+        } catch (IOException e) {
182
+            throw new RuntimeException("处理章节上传失败: " + e.getMessage());
183
+        }
184
+    }
185
+    @Override
186
+    public List<AuthorApplication> getAuthorApplications(Integer status) {
187
+        AuthorApplication app = new AuthorApplication();
188
+        app.setStatus(status);
189
+        return authorApplicationMapper.selectAuthorApplicationList(app);
190
+    }
23 191
 
24 192
     @Override
193
+    @Transactional
194
+    public void approveAuthorApplication(Long id) {
195
+        AuthorApplication application = authorApplicationMapper.selectAuthorApplicationById(id);
196
+        if (application == null) {
197
+            throw new RuntimeException("申请不存在");
198
+        }
199
+
200
+        // 更新申请状态
201
+        application.setStatus(1); // 1:已批准
202
+        authorApplicationMapper.updateAuthorApplication(application);
203
+
204
+        // 更新用户角色 (若依标准方式)
205
+        SysUser user = userMapper.selectUserById(application.getUserId());
206
+        if (user != null) {
207
+            // 添加作家角色 (假设作家角色ID=100)
208
+            Set<Long> roleIds = new HashSet<>(List.of(user.getRoleIds()));
209
+            roleIds.add(100L);
210
+            user.setRoleIds(new ArrayList<>(roleIds).toArray(new Long[0]));
211
+            userMapper.updateUser(user);
212
+        }
213
+    }
214
+
215
+
216
+    @Override
217
+    @Transactional
218
+    public void rejectAuthorApplication(Long id) {
219
+        AuthorApplication application = authorApplicationMapper.selectAuthorApplicationById(id);
220
+        if (application == null) {
221
+            throw new RuntimeException("申请不存在");
222
+        }
223
+
224
+        // 更新申请状态
225
+        application.setStatus(2); // 2:已拒绝
226
+        authorApplicationMapper.updateAuthorApplication(application);
227
+    }
228
+
229
+
230
+    @Override
231
+    public List<Category> getAllCategories() {
232
+        return categoryMapper.selectCategoryList(new Category());
233
+    }
234
+
235
+    @Override
236
+    public List<Novel> getHotNovels() {
237
+        // 获取最近一周热门小说
238
+        Map<String, Object> params = new HashMap<>();
239
+        params.put("beginTime", DateUtils.addDays(new Date(), -7));
240
+        return novelMapper.selectHotNovels(params);
241
+    }
242
+
243
+    @Override
244
+    public TableDataInfo getNovelsByCategory(Long categoryId) {
245
+        // 使用若依分页工具
246
+        startPage();
247
+
248
+        Novel novel = new Novel();
249
+        if (categoryId != null && categoryId > 0) {
250
+            novel.setCategoryId(categoryId);
251
+        }
252
+
253
+        List<Novel> list = novelMapper.selectNovelList(novel);
254
+        return getDataTable(list);
255
+    }
256
+
257
+    @Override
258
+    public List<Chapter> getChaptersByNovelId(Long novelId) {
259
+        Chapter chapter = new Chapter();
260
+        chapter.setNovelId(novelId);
261
+        return chapterMapper.selectChapterList(chapter);
262
+    }
263
+
264
+    @Override
265
+    public Chapter getChapterContent(Long chapterId) {
266
+        return chapterMapper.selectChapterById(chapterId);
267
+    }
268
+
269
+    @Override
270
+    @Transactional
271
+    public void submitAuthorApplication(AuthorApplicationDTO dto, Long userId) {
272
+        // 检查是否已有待审核申请
273
+        AuthorApplication exist = authorApplicationMapper.selectPendingApplication(userId);
274
+        if (exist != null) {
275
+            throw new RuntimeException("您已提交过申请,请等待审核");
276
+        }
277
+
278
+        // 创建申请记录
279
+        AuthorApplication application = new AuthorApplication();
280
+        application.setUserId(userId);
281
+        application.setRealName(dto.getRealName());
282
+        application.setContact(dto.getContact());
283
+        application.setNovelType(dto.getNovelType());
284
+        application.setNovelTitle(dto.getNovelTitle());
285
+        application.setNovelDescription(dto.getDescription());
286
+        application.setSampleContent(dto.getSampleContent());
287
+        application.setStatus(0); // 0:待审核
288
+        application.setCreateTime(new Date());
289
+
290
+        authorApplicationMapper.insertAuthorApplication(application);
291
+    }
292
+    /**
293
+     * 设置请求分页数据
294
+     */
295
+    protected void startPage() {
296
+        PageDomain pageDomain = TableSupport.buildPageRequest();
297
+        Integer pageNum = pageDomain.getPageNum();
298
+        Integer pageSize = pageDomain.getPageSize();
299
+        if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize)) {
300
+            String orderBy = pageDomain.getOrderBy();
301
+            PageUtils.startPage(pageNum, pageSize, orderBy);
302
+        }
303
+    }
304
+
305
+    /**
306
+     * 响应请求分页数据
307
+     */
308
+    @SuppressWarnings({"rawtypes", "unchecked"})
309
+    protected TableDataInfo getDataTable(List<?> list) {
310
+        TableDataInfo rspData = new TableDataInfo();
311
+        rspData.setCode(HttpStatus.SUCCESS);
312
+        rspData.setMsg("查询成功");
313
+        rspData.setRows(list);
314
+        rspData.setTotal(PageUtils.getLocalPage().getTotal());
315
+        return rspData;
316
+    }
317
+    @Override
25 318
     public List<Novel> selectNovelList(Novel novel) {
26 319
         return novelMapper.selectNovelList(novel);
27 320
     }

+ 52
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/utils/JwtTokenProvider.java Voir le fichier

@@ -0,0 +1,52 @@
1
+package com.ruoyi.novel.utils;
2
+
3
+import io.jsonwebtoken.Claims;
4
+import io.jsonwebtoken.Jwts;
5
+import io.jsonwebtoken.SignatureAlgorithm;
6
+import org.elasticsearch.client.security.user.User;
7
+import org.springframework.beans.factory.annotation.Value;
8
+import org.springframework.stereotype.Component;
9
+
10
+import java.util.Date;
11
+
12
+// JwtTokenProvider.java
13
+@Component
14
+public class JwtTokenProvider {
15
+
16
+    @Value("${app.jwt.secret}")
17
+    private String jwtSecret;
18
+
19
+    @Value("${app.jwt.expiration}")
20
+    private int jwtExpirationInMs;
21
+
22
+    public String generateToken(User user) {
23
+        Date now = new Date();
24
+        Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
25
+
26
+        return Jwts.builder()
27
+                .setSubject(user.getUsername())
28
+                .setIssuedAt(now)
29
+                .setExpiration(expiryDate)
30
+                .signWith(SignatureAlgorithm.HS512, jwtSecret)
31
+                .compact();
32
+    }
33
+
34
+    public boolean validateToken(String token) {
35
+        try {
36
+            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
37
+            return true;
38
+        } catch (Exception ex) {
39
+            // 处理异常
40
+        }
41
+        return false;
42
+    }
43
+
44
+    public String getUsernameFromToken(String token) {
45
+        Claims claims = Jwts.parser()
46
+                .setSigningKey(jwtSecret)
47
+                .parseClaimsJws(token)
48
+                .getBody();
49
+
50
+        return claims.getSubject();
51
+    }
52
+}

+ 3
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java Voir le fichier

@@ -124,4 +124,7 @@ public interface SysUserMapper
124 124
      * @return 结果
125 125
      */
126 126
     public SysUser checkEmailUnique(String email);
127
+
128
+//    SysUser selectUserById(Long userId);
129
+//    int updateUser(SysUser user);
127 130
 }

+ 43
- 0
RuoYi-Vue/ruoyi-system/src/main/resources/mapper/novel/NovelMapper.xml Voir le fichier

@@ -27,4 +27,47 @@
27 27
     <select id="selectList" resultMap="NovelResult">
28 28
         SELECT * FROM novel
29 29
     </select>
30
+
31
+    <select id="selectNovelList" parameterType="Novel" resultMap="NovelResult">
32
+        <include refid="selectNovelVo"/>
33
+        <where>
34
+            <if test="title != null and title != ''"> AND title LIKE CONCAT('%', #{title}, '%')</if>
35
+            <if test="authorName != null and authorName != ''"> AND author_name LIKE CONCAT('%', #{authorName}, '%')</if>
36
+            <if test="categoryId != null"> AND category_id = #{categoryId}</if>
37
+            <if test="status != null and status != ''"> AND status = #{status}</if>
38
+        </where>
39
+        ORDER BY update_time DESC
40
+    </select>
41
+
42
+    <select id="selectHotNovels" parameterType="map" resultMap="NovelResult">
43
+        <include refid="selectNovelVo"/>
44
+        WHERE update_time >= #{beginTime}
45
+        ORDER BY read_count DESC
46
+        LIMIT 10
47
+    </select>
48
+
49
+    <insert id="insertNovel" parameterType="Novel" useGeneratedKeys="true" keyProperty="id">
50
+        INSERT INTO novel (
51
+            title, author_id, author_name, cover,
52
+            description, category_id, status, create_time, update_time
53
+        ) VALUES (
54
+                     #{title}, #{authorId}, #{authorName}, #{cover},
55
+                     #{description}, #{categoryId}, #{status}, #{createTime}, #{updateTime}
56
+                 )
57
+    </insert>
58
+
59
+    <update id="updateNovel" parameterType="Novel">
60
+        UPDATE novel
61
+        <set>
62
+            <if test="title != null">title = #{title},</if>
63
+            <if test="authorId != null">author_id = #{authorId},</if>
64
+            <if test="authorName != null">author_name = #{authorName},</if>
65
+            <if test="cover != null">cover = #{cover},</if>
66
+            <if test="description != null">description = #{description},</if>
67
+            <if test="categoryId != null">category_id = #{categoryId},</if>
68
+            <if test="status != null">status = #{status},</if>
69
+            <if test="updateTime != null">update_time = #{updateTime},</if>
70
+        </set>
71
+        WHERE id = #{id}
72
+    </update>
30 73
 </mapper>

Chargement…
Annuler
Enregistrer