Просмотр исходного кода

feat: implement core backend modules - Wallet, UserProfile, Post, TipOrder, Notification, RealnameAuth, Attachment, SystemConfig

yangyi 3 дней назад
Родитель
Сommit
6fc2b31dca
63 измененных файлов с 2399 добавлено и 87 удалено
  1. 47 0
      build.gradle
  2. 0 52
      build.gradle.kts
  3. 0 0
      gradlew
  4. 227 28
      sql/postgersql.sql
  5. 41 0
      src/main/java/space/anyi/serve/controller/AttachmentController.java
  6. 12 1
      src/main/java/space/anyi/serve/controller/AuthController.java
  7. 56 0
      src/main/java/space/anyi/serve/controller/NotificationController.java
  8. 116 0
      src/main/java/space/anyi/serve/controller/PostController.java
  9. 69 0
      src/main/java/space/anyi/serve/controller/RealnameAuthController.java
  10. 38 0
      src/main/java/space/anyi/serve/controller/SystemConfigController.java
  11. 64 0
      src/main/java/space/anyi/serve/controller/TipOrderController.java
  12. 54 6
      src/main/java/space/anyi/serve/controller/UserController.java
  13. 100 0
      src/main/java/space/anyi/serve/controller/WalletController.java
  14. 33 0
      src/main/java/space/anyi/serve/entity/attachment/Attachment.java
  15. 39 0
      src/main/java/space/anyi/serve/entity/notification/Notification.java
  16. 48 0
      src/main/java/space/anyi/serve/entity/notification/NotificationVo.java
  17. 43 0
      src/main/java/space/anyi/serve/entity/order/OrderTip.java
  18. 59 0
      src/main/java/space/anyi/serve/entity/order/OrderTipVo.java
  19. 55 0
      src/main/java/space/anyi/serve/entity/post/Post.java
  20. 40 0
      src/main/java/space/anyi/serve/entity/post/PostDto.java
  21. 33 0
      src/main/java/space/anyi/serve/entity/post/PostViewRecord.java
  22. 78 0
      src/main/java/space/anyi/serve/entity/post/PostVo.java
  23. 33 0
      src/main/java/space/anyi/serve/entity/profile/UserProfile.java
  24. 22 0
      src/main/java/space/anyi/serve/entity/profile/UserProfileVo.java
  25. 48 0
      src/main/java/space/anyi/serve/entity/realname/RealnameAuth.java
  26. 60 0
      src/main/java/space/anyi/serve/entity/realname/RealnameAuthVo.java
  27. 31 0
      src/main/java/space/anyi/serve/entity/wallet/Wallet.java
  28. 49 0
      src/main/java/space/anyi/serve/entity/wallet/WalletTransaction.java
  29. 57 0
      src/main/java/space/anyi/serve/entity/wallet/WalletTransactionVo.java
  30. 9 0
      src/main/java/space/anyi/serve/mapper/AttachmentMapper.java
  31. 9 0
      src/main/java/space/anyi/serve/mapper/NotificationMapper.java
  32. 9 0
      src/main/java/space/anyi/serve/mapper/OrderTipMapper.java
  33. 9 0
      src/main/java/space/anyi/serve/mapper/PostMapper.java
  34. 9 0
      src/main/java/space/anyi/serve/mapper/PostViewRecordMapper.java
  35. 9 0
      src/main/java/space/anyi/serve/mapper/RealnameAuthMapper.java
  36. 9 0
      src/main/java/space/anyi/serve/mapper/UserProfileMapper.java
  37. 9 0
      src/main/java/space/anyi/serve/mapper/WalletMapper.java
  38. 9 0
      src/main/java/space/anyi/serve/mapper/WalletTransactionMapper.java
  39. 8 0
      src/main/java/space/anyi/serve/service/AttachmentService.java
  40. 2 0
      src/main/java/space/anyi/serve/service/MetaService.java
  41. 14 0
      src/main/java/space/anyi/serve/service/NotificationService.java
  42. 16 0
      src/main/java/space/anyi/serve/service/PostService.java
  43. 10 0
      src/main/java/space/anyi/serve/service/RealnameAuthService.java
  44. 11 0
      src/main/java/space/anyi/serve/service/TipOrderService.java
  45. 9 0
      src/main/java/space/anyi/serve/service/UserProfileService.java
  46. 18 0
      src/main/java/space/anyi/serve/service/WalletService.java
  47. 25 0
      src/main/java/space/anyi/serve/service/impl/AttachmentServiceImpl.java
  48. 14 0
      src/main/java/space/anyi/serve/service/impl/MetaServiceImpl.java
  49. 59 0
      src/main/java/space/anyi/serve/service/impl/NotificationServiceImpl.java
  50. 175 0
      src/main/java/space/anyi/serve/service/impl/PostServiceImpl.java
  51. 78 0
      src/main/java/space/anyi/serve/service/impl/RealnameAuthServiceImpl.java
  52. 87 0
      src/main/java/space/anyi/serve/service/impl/TipOrderServiceImpl.java
  53. 31 0
      src/main/java/space/anyi/serve/service/impl/UserProfileServiceImpl.java
  54. 173 0
      src/main/java/space/anyi/serve/service/impl/WalletServiceImpl.java
  55. 4 0
      src/main/resources/mapper/AttachmentMapper.xml
  56. 4 0
      src/main/resources/mapper/NotificationMapper.xml
  57. 4 0
      src/main/resources/mapper/OrderTipMapper.xml
  58. 4 0
      src/main/resources/mapper/PostMapper.xml
  59. 4 0
      src/main/resources/mapper/PostViewRecordMapper.xml
  60. 4 0
      src/main/resources/mapper/RealnameAuthMapper.xml
  61. 4 0
      src/main/resources/mapper/UserProfileMapper.xml
  62. 4 0
      src/main/resources/mapper/WalletMapper.xml
  63. 4 0
      src/main/resources/mapper/WalletTransactionMapper.xml

+ 47 - 0
build.gradle

@@ -0,0 +1,47 @@
+plugins {
+    id 'java'
+    id 'org.springframework.boot' version '3.5.13'
+    id 'io.spring.dependency-management' version '1.1.7'
+}
+
+group = 'space.anyi'
+version = '0.0.1-SNAPSHOT'
+
+java {
+    toolchain {
+        languageVersion = JavaLanguageVersion.of(17)
+    }
+}
+
+configurations {
+    compileOnly {
+        extendsFrom(configurations.annotationProcessor)
+    }
+}
+
+repositories {
+    mavenCentral()
+}
+
+dependencies {
+    implementation 'org.springframework.boot:spring-boot-starter-actuator'
+    implementation 'com.baomidou:mybatis-plus-spring-boot3-starter:3.5.15'
+    implementation 'org.springframework.boot:spring-boot-starter-security'
+    implementation 'org.springframework.boot:spring-boot-starter-validation'
+    implementation 'org.springframework.boot:spring-boot-starter-web'
+    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.16'
+    implementation 'com.alibaba:druid-spring-boot-3-starter:1.2.28'
+    developmentOnly 'org.springframework.boot:spring-boot-devtools'
+    implementation 'org.postgresql:postgresql'
+    implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
+    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
+    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
+    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
+    testImplementation 'org.springframework.boot:spring-boot-starter-test'
+    testImplementation 'org.springframework.security:spring-security-test'
+    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+}
+
+tasks.named('test') {
+    useJUnitPlatform()
+}

+ 0 - 52
build.gradle.kts

@@ -1,52 +0,0 @@
-plugins {
-    java
-    id("org.springframework.boot") version "3.5.13"
-    id("io.spring.dependency-management") version "1.1.7"
-}
-
-group = "space.anyi"
-version = "0.0.1-SNAPSHOT"
-description = "serve"
-
-java {
-    toolchain {
-        languageVersion = JavaLanguageVersion.of(17)
-    }
-}
-
-configurations {
-    compileOnly {
-        extendsFrom(configurations.annotationProcessor.get())
-    }
-}
-
-repositories {
-    mavenCentral()
-}
-
-dependencies {
-    implementation("org.springframework.boot:spring-boot-starter-actuator")
-    // Source: https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter
-    implementation ("com.baomidou:mybatis-plus-spring-boot3-starter:3.5.15")
-//    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
-    implementation("org.springframework.boot:spring-boot-starter-security")
-    implementation("org.springframework.boot:spring-boot-starter-validation")
-    implementation("org.springframework.boot:spring-boot-starter-web")
-    implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.16")
-    // Source: https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-3-starter
-    implementation("com.alibaba:druid-spring-boot-3-starter:1.2.28")
-    developmentOnly("org.springframework.boot:spring-boot-devtools")
-    implementation("org.postgresql:postgresql")
-    // JWT
-    implementation("io.jsonwebtoken:jjwt-api:0.12.6")
-    runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
-    runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
-    annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
-    testImplementation("org.springframework.boot:spring-boot-starter-test")
-    testImplementation("org.springframework.security:spring-security-test")
-    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
-}
-
-tasks.withType<Test> {
-    useJUnitPlatform()
-}

+ 0 - 0
gradlew → gradlew


+ 227 - 28
sql/postgersql.sql

@@ -5,34 +5,57 @@
 CREATE SCHEMA IF NOT EXISTS dev;
 
 
--- 删除序列(如果存在)
--- DROP SEQUENCE IF EXISTS dev.user_id_seq;
-
--- 创建新序列
--- CREATE SEQUENCE dev.user_id_seq
---     START WITH 1
---     INCREMENT BY 1
---     NO MINVALUE
---     NO MAXVALUE
---     CACHE 1;
-
--- 添加注释
--- COMMENT ON SEQUENCE dev.user_seq IS '用户表ID序列';
-
 DROP TABLE IF EXISTS dev.user;
 -- 在 dev schema 下创建用户表
 CREATE TABLE dev.user (
-                          id SERIAL PRIMARY KEY,
-                          account VARCHAR(50) NOT NULL UNIQUE,
-                          password VARCHAR(255) NOT NULL,
-                          username VARCHAR(100) NOT NULL,
-                          role VARCHAR(20) NOT NULL DEFAULT 'user' CHECK ( role IN ('user','expert','admin') ),
-                          avatar TEXT,
-                          phone_number char(11),
-                          enable INT NOT NULL DEFAULT 1,
-                          delete_flag INT NOT NULL DEFAULT 0
+    id SERIAL PRIMARY KEY,
+    account VARCHAR(50) NOT NULL UNIQUE,
+    password VARCHAR(255) NOT NULL,
+    username VARCHAR(100) NOT NULL,
+    role VARCHAR(20) NOT NULL DEFAULT 'user' CHECK ( role IN ('user','expert','admin') ),
+    avatar TEXT,
+    phone_number char(11),
+    enable INT NOT NULL DEFAULT 1,
+    delete_flag INT NOT NULL DEFAULT 0
+);
+
+-- =============================================
+-- 钱包表
+-- =============================================
+DROP TABLE IF EXISTS dev.wallet;
+CREATE TABLE dev.wallet (
+    id          BIGSERIAL       PRIMARY KEY,
+    user_id     BIGINT          NOT NULL UNIQUE,
+    balance     DECIMAL(10,2)   NOT NULL DEFAULT 0,
+    created_at  TIMESTAMP       NOT NULL DEFAULT now(),
+    updated_at  TIMESTAMP       NOT NULL DEFAULT now()
+);
+
+COMMENT ON TABLE dev.wallet IS '钱包表';
+COMMENT ON COLUMN dev.wallet.user_id IS '用户ID';
+COMMENT ON COLUMN dev.wallet.balance IS '钱包余额';
+
+CREATE UNIQUE INDEX idx_wallet_user_id ON dev.wallet(user_id);
+
+-- =============================================
+-- 用户扩展信息表
+-- =============================================
+DROP TABLE IF EXISTS dev.user_profile;
+CREATE TABLE dev.user_profile (
+    id          BIGSERIAL   PRIMARY KEY,
+    user_id     BIGINT      NOT NULL UNIQUE,
+    level       VARCHAR(20) NOT NULL DEFAULT 'gold' CHECK ( level IN ('gold','diamond','master') ),
+    is_realname BOOLEAN     DEFAULT FALSE,
+    created_at  TIMESTAMP   NOT NULL DEFAULT now(),
+    updated_at  TIMESTAMP   NOT NULL DEFAULT now()
 );
 
+COMMENT ON TABLE dev.user_profile IS '用户扩展信息表';
+COMMENT ON COLUMN dev.user_profile.level IS '等级:gold黄金 diamond钻石 master大师';
+COMMENT ON COLUMN dev.user_profile.is_realname IS '是否已实名认证';
+
+CREATE UNIQUE INDEX idx_up_user_id ON dev.user_profile(user_id);
+
 -- 添加注释
 COMMENT ON TABLE dev.user IS '用户表';
 COMMENT ON COLUMN dev.user.id IS '用户ID,自增主键';
@@ -72,9 +95,185 @@ CREATE UNIQUE INDEX idx_meta_key ON dev.meta (key);
 CREATE INDEX idx_meta_value_gin ON dev.meta USING GIN (value);
 
 INSERT INTO dev.meta(key, value) VALUES
-                                     ('website_config','{
-                                       "title": "示例网站",
+                                     ('site','{
+                                       "title": "咕咕嘎嘎论坛",
                                        "logo": "",
-                                       "announcement": "欢迎访问我们的网站!",
-                                       "statement": "版权所有 © 2025 示例网站"
-                                     }');
+                                       "announcement": "欢迎访问咕咕嘎嘎论坛!",
+                                       "statement": "本站所有内容仅代表发布者个人观点,平台对内容的真实性、完整性、及时性不做任何保证,请用户理性参考,谨慎打赏。"
+                                     }');
+
+-- =============================================
+-- 帖子表
+-- =============================================
+DROP TABLE IF EXISTS dev.post;
+CREATE TABLE dev.post (
+    id              BIGSERIAL       PRIMARY KEY,
+    expert_id       BIGINT          NOT NULL,
+    title           VARCHAR(200)    NOT NULL,
+    content_intro   TEXT,
+    content_paid    TEXT,
+    price           DECIMAL(10,2)   NOT NULL,
+    hit_status      VARCHAR(20)     NOT NULL DEFAULT 'pending' CHECK ( hit_status IN ('pending','hit','miss') ),
+    view_count      INT             NOT NULL DEFAULT 0,
+    publish_time    TIMESTAMP       NOT NULL DEFAULT now(),
+    expire_time     TIMESTAMP       NOT NULL,
+    delete_flag     INT             DEFAULT 0,
+    created_at      TIMESTAMP       NOT NULL DEFAULT now(),
+    updated_at      TIMESTAMP       NOT NULL DEFAULT now()
+);
+
+COMMENT ON TABLE dev.post IS '帖子表';
+COMMENT ON COLUMN dev.post.id IS '帖子ID';
+COMMENT ON COLUMN dev.post.expert_id IS '发布专家ID';
+COMMENT ON COLUMN dev.post.title IS '帖子标题(含期号)';
+COMMENT ON COLUMN dev.post.content_intro IS '内容简介(公开)';
+COMMENT ON COLUMN dev.post.content_paid IS '付费内容';
+COMMENT ON COLUMN dev.post.price IS '打赏金额';
+COMMENT ON COLUMN dev.post.hit_status IS '命中状态:pending待确认 hit命中 miss未命中';
+COMMENT ON COLUMN dev.post.view_count IS '查看人数(管理员可修改)';
+COMMENT ON COLUMN dev.post.publish_time IS '发布时间';
+COMMENT ON COLUMN dev.post.expire_time IS '过期时间,超出此时间自动转为公开';
+
+CREATE INDEX idx_post_expert_id ON dev.post(expert_id);
+CREATE INDEX idx_post_hit_status ON dev.post(hit_status);
+CREATE INDEX idx_post_expire_time ON dev.post(expire_time);
+CREATE INDEX idx_post_delete_flag ON dev.post(delete_flag);
+
+-- =============================================
+-- 帖子访问记录表
+-- =============================================
+DROP TABLE IF EXISTS dev.post_view_record;
+CREATE TABLE dev.post_view_record (
+    id          BIGSERIAL   PRIMARY KEY,
+    post_id     BIGINT      NOT NULL,
+    user_id     BIGINT      NOT NULL,
+    view_time   TIMESTAMP   NOT NULL DEFAULT now(),
+    created_at  TIMESTAMP   NOT NULL DEFAULT now(),
+    updated_at  TIMESTAMP   NOT NULL DEFAULT now()
+);
+
+COMMENT ON TABLE dev.post_view_record IS '帖子访问记录表';
+COMMENT ON COLUMN dev.post_view_record.post_id IS '帖子ID';
+COMMENT ON COLUMN dev.post_view_record.user_id IS '访问用户ID';
+
+CREATE UNIQUE INDEX idx_pvr_post_user ON dev.post_view_record(post_id, user_id);
+
+-- =============================================
+-- 打赏订单表
+-- =============================================
+DROP TABLE IF EXISTS dev.order_tip;
+CREATE TABLE dev.order_tip (
+    id          BIGSERIAL       PRIMARY KEY,
+    user_id     BIGINT          NOT NULL,
+    post_id     BIGINT          NOT NULL,
+    expert_id   BIGINT          NOT NULL,
+    amount      DECIMAL(10,2)   NOT NULL,
+    status      VARCHAR(20)     NOT NULL DEFAULT 'completed',
+    create_time TIMESTAMP       NOT NULL DEFAULT now(),
+    created_at  TIMESTAMP       NOT NULL DEFAULT now(),
+    updated_at  TIMESTAMP       NOT NULL DEFAULT now()
+);
+
+COMMENT ON TABLE dev.order_tip IS '打赏订单表';
+COMMENT ON COLUMN dev.order_tip.user_id IS '打赏人ID';
+COMMENT ON COLUMN dev.order_tip.post_id IS '帖子ID';
+COMMENT ON COLUMN dev.order_tip.expert_id IS '被打赏专家ID';
+COMMENT ON COLUMN dev.order_tip.amount IS '打赏金额';
+
+CREATE INDEX idx_ot_user_id ON dev.order_tip(user_id);
+CREATE INDEX idx_ot_post_id ON dev.order_tip(post_id);
+CREATE INDEX idx_ot_expert_id ON dev.order_tip(expert_id);
+
+-- =============================================
+-- 钱包流水表
+-- =============================================
+DROP TABLE IF EXISTS dev.wallet_transaction;
+CREATE TABLE dev.wallet_transaction (
+    id              BIGSERIAL       PRIMARY KEY,
+    user_id         BIGINT          NOT NULL,
+    type            VARCHAR(20)     NOT NULL,
+    amount          DECIMAL(10,2)   NOT NULL,
+    balance_before  DECIMAL(10,2),
+    balance_after   DECIMAL(10,2),
+    status          VARCHAR(20)     NOT NULL DEFAULT 'success',
+    remark          VARCHAR(255),
+    review_time     TIMESTAMP,
+    created_at      TIMESTAMP       NOT NULL DEFAULT now(),
+    updated_at      TIMESTAMP       NOT NULL DEFAULT now()
+);
+
+COMMENT ON TABLE dev.wallet_transaction IS '钱包流水表';
+COMMENT ON COLUMN dev.wallet_transaction.user_id IS '用户ID';
+COMMENT ON COLUMN dev.wallet_transaction.type IS '流水类型:recharge充值 withdraw提现 tip_out打赏支出 admin_adjust管理员调整';
+COMMENT ON COLUMN dev.wallet_transaction.amount IS '金额,正数为收入,负数为支出';
+
+CREATE INDEX idx_wt_user_id ON dev.wallet_transaction(user_id);
+CREATE INDEX idx_wt_type ON dev.wallet_transaction(type);
+
+-- =============================================
+-- 实名认证表
+-- =============================================
+DROP TABLE IF EXISTS dev.realname_auth;
+CREATE TABLE dev.realname_auth (
+    id              BIGSERIAL       PRIMARY KEY,
+    user_id         BIGINT          NOT NULL UNIQUE,
+    real_name       VARCHAR(50)     NOT NULL,
+    id_card         VARCHAR(18)     NOT NULL,
+    id_card_front   TEXT,
+    id_card_back    TEXT,
+    status          VARCHAR(20)     NOT NULL DEFAULT 'pending' CHECK ( status IN ('pending','approved','rejected') ),
+    reject_reason   VARCHAR(255),
+    review_time     TIMESTAMP,
+    created_at      TIMESTAMP       NOT NULL DEFAULT now(),
+    updated_at      TIMESTAMP       NOT NULL DEFAULT now()
+);
+
+COMMENT ON TABLE dev.realname_auth IS '实名认证表';
+COMMENT ON COLUMN dev.realname_auth.user_id IS '用户ID';
+COMMENT ON COLUMN dev.realname_auth.real_name IS '真实姓名';
+COMMENT ON COLUMN dev.realname_auth.id_card IS '身份证号';
+COMMENT ON COLUMN dev.realname_auth.status IS '认证状态:pending待审核 approved通过 rejected驳回';
+
+CREATE UNIQUE INDEX idx_ra_user_id ON dev.realname_auth(user_id);
+
+-- =============================================
+-- 系统通知表
+-- =============================================
+DROP TABLE IF EXISTS dev.notification;
+CREATE TABLE dev.notification (
+    id          BIGSERIAL       PRIMARY KEY,
+    user_id     BIGINT          NOT NULL,
+    type        VARCHAR(30)     NOT NULL,
+    title       VARCHAR(100),
+    content     TEXT,
+    is_read     BOOLEAN         DEFAULT FALSE,
+    created_at  TIMESTAMP       NOT NULL DEFAULT now(),
+    updated_at  TIMESTAMP       NOT NULL DEFAULT now()
+);
+
+COMMENT ON TABLE dev.notification IS '系统通知表';
+COMMENT ON COLUMN dev.notification.user_id IS '接收人ID';
+COMMENT ON COLUMN dev.notification.type IS '通知类型:tip_success recharge_success withdraw_request withdraw_success withdraw_failed system';
+
+CREATE INDEX idx_notif_user_id ON dev.notification(user_id);
+CREATE INDEX idx_notif_is_read ON dev.notification(is_read);
+
+-- =============================================
+-- OSS附件表
+-- =============================================
+DROP TABLE IF EXISTS dev.attachment;
+CREATE TABLE dev.attachment (
+    id          BIGSERIAL   PRIMARY KEY,
+    user_id     BIGINT      NOT NULL,
+    url         TEXT        NOT NULL,
+    type        VARCHAR(20),
+    created_at  TIMESTAMP   NOT NULL DEFAULT now(),
+    updated_at  TIMESTAMP   NOT NULL DEFAULT now()
+);
+
+COMMENT ON TABLE dev.attachment IS 'OSS附件表';
+COMMENT ON COLUMN dev.attachment.user_id IS '上传人ID';
+COMMENT ON COLUMN dev.attachment.url IS 'OSS文件URL';
+COMMENT ON COLUMN dev.attachment.type IS '附件类型:avatar头像 id_card身份证 post_image帖子图片';
+
+CREATE INDEX idx_attachment_user_id ON dev.attachment(user_id);

+ 41 - 0
src/main/java/space/anyi/serve/controller/AttachmentController.java

@@ -0,0 +1,41 @@
+package space.anyi.serve.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import space.anyi.serve.entity.Response;
+import space.anyi.serve.entity.auth.JwtUserDetails;
+import space.anyi.serve.service.AttachmentService;
+
+import java.util.Map;
+
+@Tag(name = "AttachmentController", description = "附件上传")
+@RestController
+@RequestMapping("api/attachments")
+public class AttachmentController {
+
+    private final AttachmentService attachmentService;
+
+    public AttachmentController(AttachmentService attachmentService) {
+        this.attachmentService = attachmentService;
+    }
+
+    @Operation(summary = "上传附件到OSS(需对接OSS配置)")
+    @PreAuthorize("hasAnyRole('ROLE_user', 'ROLE_expert', 'ROLE_admin')")
+    @PostMapping("upload")
+    public Response<Map<String, String>> upload(
+            @RequestParam("file") MultipartFile file,
+            @RequestParam(defaultValue = "post_image") String type,
+            Authentication authentication) {
+        JwtUserDetails details = (JwtUserDetails) authentication.getPrincipal();
+        Long userId = details.getUser().getId();
+
+        // TODO: 对接 OSS 上传,当前返回占位 URL
+        String url = "https://oss.example.com/" + System.currentTimeMillis() + "_" + file.getOriginalFilename();
+        attachmentService.upload(userId, url, type);
+        return Response.ok(Map.of("url", url));
+    }
+}

+ 12 - 1
src/main/java/space/anyi/serve/controller/AuthController.java

@@ -19,6 +19,8 @@ import space.anyi.serve.entity.auth.RegisterDto;
 import space.anyi.serve.entity.user.User;
 import space.anyi.serve.handler.security.JwtTokenProvider;
 import space.anyi.serve.service.UserService;
+import space.anyi.serve.service.WalletService;
+import space.anyi.serve.service.UserProfileService;
 
 @Tag(name = "AuthController",description = "用户认证相关接口")
 @RestController
@@ -29,15 +31,22 @@ public class AuthController {
     private final AuthenticationManager authenticationManager;
     private final JwtTokenProvider jwtTokenProvider;
     private final PasswordEncoder passwordEncoder;
+    private final WalletService walletService;
+    private final UserProfileService userProfileService;
 
 
     public AuthController(UserService userService,
                           AuthenticationManager authenticationManager,
-                          JwtTokenProvider jwtTokenProvider, PasswordEncoder passwordEncoder) {
+                          JwtTokenProvider jwtTokenProvider,
+                          PasswordEncoder passwordEncoder,
+                          WalletService walletService,
+                          UserProfileService userProfileService) {
         this.userService = userService;
         this.authenticationManager = authenticationManager;
         this.jwtTokenProvider = jwtTokenProvider;
         this.passwordEncoder = passwordEncoder;
+        this.walletService = walletService;
+        this.userProfileService = userProfileService;
     }
 
     @Operation(summary = "用户登录")
@@ -65,6 +74,8 @@ public class AuthController {
         user.setPassword(passwordEncoder.encode(registerDto.getPassword()));
         user.setUsername(registerDto.getUsername());
         userService.save(user);
+        walletService.createWallet(user.getId());
+        userProfileService.createProfile(user.getId());
         String token = jwtTokenProvider.generateToken(user.getId(), user.getAccount(), user.getUsername(), user.getRole());
         AuthTokenVo authTokenVo = new AuthTokenVo(token, user.getId(), user.getAccount(), user.getUsername(), user.getRole());
         return Response.ok(authTokenVo);

+ 56 - 0
src/main/java/space/anyi/serve/controller/NotificationController.java

@@ -0,0 +1,56 @@
+package space.anyi.serve.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.*;
+import space.anyi.serve.entity.Response;
+import space.anyi.serve.entity.auth.JwtUserDetails;
+import space.anyi.serve.entity.notification.NotificationVo;
+import space.anyi.serve.service.NotificationService;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "NotificationController", description = "系统通知")
+@RestController
+@RequestMapping("api/notifications")
+public class NotificationController {
+
+    private final NotificationService notificationService;
+
+    public NotificationController(NotificationService notificationService) {
+        this.notificationService = notificationService;
+    }
+
+    @Operation(summary = "获取通知列表")
+    @PreAuthorize("hasAnyRole('ROLE_user', 'ROLE_expert', 'ROLE_admin')")
+    @GetMapping
+    public Response<List<NotificationVo>> listNotifications(Authentication authentication) {
+        JwtUserDetails details = (JwtUserDetails) authentication.getPrincipal();
+        Long userId = details.getUser().getId();
+        return Response.ok(notificationService.listNotifications(userId));
+    }
+
+    @Operation(summary = "获取未读数")
+    @PreAuthorize("hasAnyRole('ROLE_user', 'ROLE_expert', 'ROLE_admin')")
+    @GetMapping("unread-count")
+    public Response<Map<String, Long>> getUnreadCount(Authentication authentication) {
+        JwtUserDetails details = (JwtUserDetails) authentication.getPrincipal();
+        Long userId = details.getUser().getId();
+        long count = notificationService.getUnreadCount(userId);
+        return Response.ok(Map.of("count", count));
+    }
+
+    @Operation(summary = "标记已读")
+    @PreAuthorize("hasAnyRole('ROLE_user', 'ROLE_expert', 'ROLE_admin')")
+    @PutMapping("{id}/read")
+    public Response<Void> markRead(@PathVariable Long id, Authentication authentication) {
+        JwtUserDetails details = (JwtUserDetails) authentication.getPrincipal();
+        Long userId = details.getUser().getId();
+        notificationService.markRead(id, userId);
+        return Response.ok();
+    }
+}

+ 116 - 0
src/main/java/space/anyi/serve/controller/PostController.java

@@ -0,0 +1,116 @@
+package space.anyi.serve.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.*;
+import space.anyi.serve.entity.PageVo;
+import space.anyi.serve.entity.Response;
+import space.anyi.serve.entity.auth.JwtUserDetails;
+import space.anyi.serve.entity.post.Post;
+import space.anyi.serve.entity.post.PostDto;
+import space.anyi.serve.entity.post.PostVo;
+import space.anyi.serve.entity.user.User;
+import space.anyi.serve.service.PostService;
+import space.anyi.serve.service.UserService;
+
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "PostController", description = "帖子管理")
+@RestController
+@RequestMapping("api/posts")
+public class PostController {
+
+    private final PostService postService;
+    private final UserService userService;
+
+    public PostController(PostService postService, UserService userService) {
+        this.postService = postService;
+        this.userService = userService;
+    }
+
+    @Operation(summary = "获取帖子列表")
+    @GetMapping
+    public Response<PageVo<List<PostVo>>> listPosts(
+            @RequestParam(defaultValue = "") String keyword,
+            @RequestParam(defaultValue = "all") String status,
+            @RequestParam(defaultValue = "1") int pageNum,
+            @RequestParam(defaultValue = "10") int pageSize,
+            Authentication authentication) {
+        Long userId = null;
+        if (authentication != null && authentication.isAuthenticated()) {
+            JwtUserDetails details = (JwtUserDetails) authentication.getPrincipal();
+            userId = details.getUser().getId();
+        }
+        Page<PostVo> page = postService.listPosts(keyword, status, pageNum, pageSize, userId);
+        return Response.ok(new PageVo<>(page.getTotal(), page.getRecords()));
+    }
+
+    @Operation(summary = "获取帖子详情")
+    @GetMapping("{id}")
+    public Response<PostVo> getPostDetail(@PathVariable Long id, Authentication authentication) {
+        Long userId = null;
+        if (authentication != null && authentication.isAuthenticated()) {
+            JwtUserDetails details = (JwtUserDetails) authentication.getPrincipal();
+            userId = details.getUser().getId();
+        }
+        PostVo vo = postService.getPostDetail(id, userId);
+        if (vo == null) return Response.error("帖子不存在");
+        return Response.ok(vo);
+    }
+
+    @Operation(summary = "创建帖子(仅专家/管理员)")
+    @PreAuthorize("hasAnyRole('ROLE_expert', 'ROLE_admin')")
+    @PostMapping
+    public Response<Map<String, Object>> createPost(@Valid @RequestBody PostDto dto, Authentication authentication) {
+        JwtUserDetails details = (JwtUserDetails) authentication.getPrincipal();
+        User currentUser = userService.queryById(details.getUser().getId());
+
+        Post post = new Post();
+        post.setExpertId(currentUser.getId());
+        post.setTitle(dto.getTitle());
+        post.setContentIntro(dto.getContentIntro());
+        post.setContentPaid(dto.getContentPaid());
+        post.setPrice(dto.getPrice());
+        post.setExpireTime(dto.getExpireTime());
+        Long postId = postService.createPost(post);
+        return Response.ok(Map.of("id", postId.toString()));
+    }
+
+    @Operation(summary = "设置命中状态(管理员)")
+    @PreAuthorize("hasRole('ROLE_admin')")
+    @PutMapping("{id}/hit-status")
+    public Response<Void> updateHitStatus(@PathVariable Long id, @RequestBody Map<String, String> body) {
+        String hitStatus = body.get("hitStatus");
+        if (hitStatus == null || (!hitStatus.equals("pending") && !hitStatus.equals("hit") && !hitStatus.equals("miss"))) {
+            return Response.error("无效的命中状态");
+        }
+        postService.updateHitStatus(id, hitStatus);
+        return Response.ok();
+    }
+
+    @Operation(summary = "修改查看人数(管理员)")
+    @PreAuthorize("hasRole('ROLE_admin')")
+    @PutMapping("{id}/view-count")
+    public Response<Void> updateViewCount(@PathVariable Long id, @RequestBody Map<String, Integer> body) {
+        Integer viewCount = body.get("viewCount");
+        if (viewCount == null || viewCount < 0) return Response.error("无效的查看人数");
+        postService.updateViewCount(id, viewCount);
+        return Response.ok();
+    }
+
+    @Operation(summary = "获取专家往期帖子")
+    @GetMapping("expert/{expertId}/previous")
+    public Response<PageVo<List<PostVo>>> listPreviousPosts(
+            @PathVariable Long expertId,
+            @RequestParam(defaultValue = "1") int pageNum,
+            @RequestParam(defaultValue = "10") int pageSize) {
+        Page<Post> page = postService.listExpertPreviousPosts(expertId, pageNum, pageSize);
+        List<PostVo> vos = PostVo.from(page.getRecords());
+        return Response.ok(new PageVo<>(page.getTotal(), vos));
+    }
+}

+ 69 - 0
src/main/java/space/anyi/serve/controller/RealnameAuthController.java

@@ -0,0 +1,69 @@
+package space.anyi.serve.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.*;
+import space.anyi.serve.entity.Response;
+import space.anyi.serve.entity.auth.JwtUserDetails;
+import space.anyi.serve.entity.realname.RealnameAuth;
+import space.anyi.serve.entity.realname.RealnameAuthVo;
+import space.anyi.serve.service.RealnameAuthService;
+
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "RealnameAuthController", description = "实名认证")
+@RestController
+@RequestMapping("api/realname")
+public class RealnameAuthController {
+
+    private final RealnameAuthService realnameAuthService;
+
+    public RealnameAuthController(RealnameAuthService realnameAuthService) {
+        this.realnameAuthService = realnameAuthService;
+    }
+
+    @Operation(summary = "提交实名认证")
+    @PreAuthorize("hasAnyRole('ROLE_user', 'ROLE_expert', 'ROLE_admin')")
+    @PostMapping
+    public Response<Void> submit(@Valid @RequestBody Map<String, String> body, Authentication authentication) {
+        JwtUserDetails details = (JwtUserDetails) authentication.getPrincipal();
+        Long userId = details.getUser().getId();
+        realnameAuthService.submit(userId, body.get("realName"), body.get("idCard"),
+                body.get("idCardFront"), body.get("idCardBack"));
+        return Response.ok();
+    }
+
+    @Operation(summary = "查询自己的认证信息")
+    @PreAuthorize("hasAnyRole('ROLE_user', 'ROLE_expert', 'ROLE_admin')")
+    @GetMapping
+    public Response<RealnameAuthVo> getMyAuth(Authentication authentication) {
+        JwtUserDetails details = (JwtUserDetails) authentication.getPrincipal();
+        Long userId = details.getUser().getId();
+        RealnameAuth auth = realnameAuthService.getByUserId(userId);
+        return Response.ok(RealnameAuthVo.from(auth));
+    }
+
+    @Operation(summary = "管理员审核实名认证")
+    @PreAuthorize("hasRole('ROLE_admin')")
+    @PutMapping("{id}/review")
+    public Response<Void> review(@PathVariable Long id, @RequestBody Map<String, Object> body) {
+        Boolean approved = (Boolean) body.get("approved");
+        String rejectReason = (String) body.get("rejectReason");
+        if (approved == null) return Response.error("请指定审核结果");
+        realnameAuthService.review(id, approved, rejectReason);
+        return Response.ok();
+    }
+
+    @Operation(summary = "获取所有待审核认证(管理员)")
+    @PreAuthorize("hasRole('ROLE_admin')")
+    @GetMapping("pending")
+    public Response<List<RealnameAuthVo>> listPending() {
+        List<RealnameAuth> list = realnameAuthService.lambdaQuery()
+                .eq(RealnameAuth::getStatus, "pending").list();
+        return Response.ok(RealnameAuthVo.from(list));
+    }
+}

+ 38 - 0
src/main/java/space/anyi/serve/controller/SystemConfigController.java

@@ -0,0 +1,38 @@
+package space.anyi.serve.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import space.anyi.serve.entity.Response;
+import space.anyi.serve.entity.meta.Meta;
+import space.anyi.serve.service.MetaService;
+
+import java.util.Map;
+
+@Tag(name = "SystemConfigController", description = "系统配置")
+@RestController
+@RequestMapping("api/config")
+public class SystemConfigController {
+
+    private final MetaService metaService;
+
+    public SystemConfigController(MetaService metaService) {
+        this.metaService = metaService;
+    }
+
+    @Operation(summary = "获取配置")
+    @GetMapping("{key}")
+    public Response<Object> getConfig(@PathVariable String key) {
+        Meta meta = metaService.getMeta(key);
+        return Response.ok(meta != null ? meta.getValue() : null);
+    }
+
+    @Operation(summary = "更新配置(管理员)")
+    @PreAuthorize("hasRole('ROLE_admin')")
+    @PutMapping("{key}")
+    public Response<Void> updateConfig(@PathVariable String key, @RequestBody Object value) {
+        metaService.updateMetaValue(key, value);
+        return Response.ok();
+    }
+}

+ 64 - 0
src/main/java/space/anyi/serve/controller/TipOrderController.java

@@ -0,0 +1,64 @@
+package space.anyi.serve.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.*;
+import space.anyi.serve.entity.PageVo;
+import space.anyi.serve.entity.Response;
+import space.anyi.serve.entity.auth.JwtUserDetails;
+import space.anyi.serve.entity.order.OrderTip;
+import space.anyi.serve.entity.order.OrderTipVo;
+import space.anyi.serve.service.TipOrderService;
+
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "TipOrderController", description = "打赏订单")
+@RestController
+@RequestMapping("api/orders")
+public class TipOrderController {
+
+    private final TipOrderService tipOrderService;
+
+    public TipOrderController(TipOrderService tipOrderService) {
+        this.tipOrderService = tipOrderService;
+    }
+
+    @Operation(summary = "打赏并即时扣款")
+    @PreAuthorize("hasAnyRole('ROLE_user', 'ROLE_expert', 'ROLE_admin')")
+    @PostMapping("tip")
+    public Response<Map<String, Object>> createAndPay(
+            @Valid @RequestBody Map<String, Long> body,
+            @RequestHeader("X-Request-Id") String requestId,
+            Authentication authentication) {
+        JwtUserDetails details = (JwtUserDetails) authentication.getPrincipal();
+        Long userId = details.getUser().getId();
+        Long postId = body.get("postId");
+        if (postId == null) return Response.error("postId不能为空");
+
+        OrderTip order = tipOrderService.createAndPay(userId, postId, requestId);
+        return Response.ok(Map.of(
+                "orderId", order.getId().toString(),
+                "status", order.getStatus(),
+                "amount", order.getAmount()
+        ));
+    }
+
+    @Operation(summary = "获取我的打赏订单列表")
+    @PreAuthorize("hasAnyRole('ROLE_user', 'ROLE_expert', 'ROLE_admin')")
+    @GetMapping
+    public Response<PageVo<List<OrderTipVo>>> listOrders(
+            @RequestParam(defaultValue = "all") String status,
+            @RequestParam(defaultValue = "1") int pageNum,
+            @RequestParam(defaultValue = "10") int pageSize,
+            Authentication authentication) {
+        JwtUserDetails details = (JwtUserDetails) authentication.getPrincipal();
+        Long userId = details.getUser().getId();
+        Page<OrderTipVo> page = tipOrderService.listOrders(userId, status, pageNum, pageSize);
+        return Response.ok(new PageVo<>(page.getTotal(), page.getRecords()));
+    }
+}

+ 54 - 6
src/main/java/space/anyi/serve/controller/UserController.java

@@ -1,7 +1,7 @@
 package space.anyi.serve.controller;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import jakarta.annotation.Resource;
+
 import jakarta.validation.Valid;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotEmpty;
@@ -11,10 +11,16 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import space.anyi.serve.entity.PageVo;
 import space.anyi.serve.entity.Response;
+import space.anyi.serve.entity.profile.UserProfile;
 import space.anyi.serve.entity.user.*;
+import space.anyi.serve.entity.wallet.Wallet;
 import space.anyi.serve.service.UserService;
+import space.anyi.serve.service.WalletService;
+import space.anyi.serve.service.UserProfileService;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 
@@ -27,11 +33,15 @@ import java.util.stream.Collectors;
 @RestController
 @RequestMapping("user")
 public class UserController {
-    /**
-     * 服务对象
-     */
-    @Resource
-    private UserService userService;
+    private final UserService userService;
+    private final WalletService walletService;
+    private final UserProfileService userProfileService;
+
+    public UserController(UserService userService, WalletService walletService, UserProfileService userProfileService) {
+        this.userService = userService;
+        this.walletService = walletService;
+        this.userProfileService = userProfileService;
+    }
 
     /**
      * 分页查询
@@ -158,5 +168,43 @@ public class UserController {
         return Response.ok(userService.updatePassword(user,dto.getOldPassword()));
     }
 
+    @PreAuthorize("hasRole('ROLE_admin')")
+    @PutMapping("/{id}/role")
+    public Response<Void> updateRole(@PathVariable Long id, @RequestBody Map<String, String> body) {
+        String role = body.get("role");
+        if (role == null || (!role.equals("user") && !role.equals("expert") && !role.equals("admin"))) {
+            return Response.error("无效的角色值");
+        }
+        User user = userService.queryById(id);
+        if (user == null) return Response.error("用户不存在");
+        user.setRole(role);
+        userService.update(user);
+        return Response.ok();
+    }
+
+    @GetMapping("/profile")
+    @PreAuthorize("hasAnyRole('ROLE_admin', 'ROLE_user', 'ROLE_expert')")
+    public Response<Map<String, Object>> profile(org.springframework.security.core.Authentication authentication) {
+        org.springframework.security.core.userdetails.UserDetails principal =
+                (org.springframework.security.core.userdetails.UserDetails) authentication.getPrincipal();
+        space.anyi.serve.entity.auth.JwtUserDetails userDetails = (space.anyi.serve.entity.auth.JwtUserDetails) principal;
+        Long userId = userDetails.getUser().getId();
+
+        User user = userService.queryById(userId);
+        Wallet wallet = walletService.getByUserId(userId);
+        UserProfile profile = userProfileService.getByUserId(userId);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("id", user.getId().toString());
+        result.put("account", user.getAccount());
+        result.put("username", user.getUsername());
+        result.put("avatar", user.getAvatar());
+        result.put("role", user.getRole());
+        result.put("phoneNumber", user.getPhoneNumber());
+        result.put("balance", wallet != null ? wallet.getBalance() : 0);
+        result.put("level", profile != null ? profile.getLevel() : "gold");
+        result.put("isRealname", profile != null ? profile.getIsRealname() : false);
+        return Response.ok(result);
+    }
 }
 

+ 100 - 0
src/main/java/space/anyi/serve/controller/WalletController.java

@@ -0,0 +1,100 @@
+package space.anyi.serve.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.*;
+import space.anyi.serve.entity.Response;
+import space.anyi.serve.entity.auth.JwtUserDetails;
+import space.anyi.serve.entity.wallet.Wallet;
+import space.anyi.serve.entity.wallet.WalletTransaction;
+import space.anyi.serve.entity.wallet.WalletTransactionVo;
+import space.anyi.serve.service.WalletService;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "WalletController", description = "钱包管理")
+@RestController
+@RequestMapping("api/wallet")
+public class WalletController {
+
+    private final WalletService walletService;
+
+    public WalletController(WalletService walletService) {
+        this.walletService = walletService;
+    }
+
+    @Operation(summary = "查询余额")
+    @PreAuthorize("hasAnyRole('ROLE_user', 'ROLE_expert', 'ROLE_admin')")
+    @GetMapping("balance")
+    public Response<Map<String, Object>> getBalance(Authentication authentication) {
+        JwtUserDetails details = (JwtUserDetails) authentication.getPrincipal();
+        Long userId = details.getUser().getId();
+        Wallet wallet = walletService.getByUserId(userId);
+        Map<String, Object> result = new HashMap<>();
+        result.put("balance", wallet != null ? wallet.getBalance() : BigDecimal.ZERO);
+        return Response.ok(result);
+    }
+
+    @Operation(summary = "充值(预留接口,待对接支付平台)")
+    @PreAuthorize("hasAnyRole('ROLE_user', 'ROLE_expert', 'ROLE_admin')")
+    @PostMapping("recharge")
+    public Response<Map<String, Object>> recharge(@RequestBody Map<String, BigDecimal> body, Authentication authentication) {
+        JwtUserDetails details = (JwtUserDetails) authentication.getPrincipal();
+        Long userId = details.getUser().getId();
+        BigDecimal amount = body.get("amount");
+        if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
+            return Response.error("充值金额必须大于0");
+        }
+        walletService.addBalance(userId, amount, "recharge", "用户充值");
+        return Response.ok(Map.of("message", "充值成功"));
+    }
+
+    @Operation(summary = "管理员代充值")
+    @PreAuthorize("hasRole('ROLE_admin')")
+    @PostMapping("admin-recharge")
+    public Response<Map<String, Object>> adminRecharge(@RequestBody Map<String, Object> body) {
+        Long userId = Long.valueOf(body.get("userId").toString());
+        BigDecimal amount = new BigDecimal(body.get("amount").toString());
+        String remark = (String) body.getOrDefault("remark", "管理员代充值");
+        if (amount.compareTo(BigDecimal.ZERO) <= 0) return Response.error("金额必须大于0");
+        walletService.addBalance(userId, amount, "admin_adjust", remark);
+        return Response.ok(Map.of("message", "充值成功"));
+    }
+
+    @Operation(summary = "提现申请")
+    @PreAuthorize("hasAnyRole('ROLE_user', 'ROLE_expert', 'ROLE_admin')")
+    @PostMapping("withdraw")
+    public Response<Map<String, Object>> applyWithdraw(@RequestBody Map<String, BigDecimal> body, Authentication authentication) {
+        JwtUserDetails details = (JwtUserDetails) authentication.getPrincipal();
+        Long userId = details.getUser().getId();
+        BigDecimal amount = body.get("amount");
+        if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
+            return Response.error("提现金额必须大于0");
+        }
+        WalletTransaction tx = walletService.applyWithdraw(userId, amount);
+        return Response.ok(Map.of("transactionId", tx.getId().toString(), "status", tx.getStatus()));
+    }
+
+    @Operation(summary = "资金明细")
+    @PreAuthorize("hasAnyRole('ROLE_user', 'ROLE_expert', 'ROLE_admin')")
+    @GetMapping("transactions")
+    public Response<Map<String, Object>> getTransactions(
+            @RequestParam(defaultValue = "1") int pageNum,
+            @RequestParam(defaultValue = "10") int pageSize,
+            Authentication authentication) {
+        JwtUserDetails details = (JwtUserDetails) authentication.getPrincipal();
+        Long userId = details.getUser().getId();
+        List<WalletTransaction> list = walletService.getTransactions(userId, pageNum, pageSize);
+        List<WalletTransactionVo> vos = WalletTransactionVo.from(list);
+        Map<String, Object> result = new HashMap<>();
+        result.put("list", vos);
+        result.put("total", vos.size());
+        return Response.ok(result);
+    }
+}

+ 33 - 0
src/main/java/space/anyi/serve/entity/attachment/Attachment.java

@@ -0,0 +1,33 @@
+package space.anyi.serve.entity.attachment;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "OSS附件")
+@TableName("dev.attachment")
+public class Attachment {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long userId;
+    private String url;
+    private String type;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getUserId() { return userId; }
+    public void setUserId(Long userId) { this.userId = userId; }
+    public String getUrl() { return url; }
+    public void setUrl(String url) { this.url = url; }
+    public String getType() { return type; }
+    public void setType(String type) { this.type = type; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+    public LocalDateTime getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+}

+ 39 - 0
src/main/java/space/anyi/serve/entity/notification/Notification.java

@@ -0,0 +1,39 @@
+package space.anyi.serve.entity.notification;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "系统通知")
+@TableName("dev.notification")
+public class Notification {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long userId;
+    private String type;
+    private String title;
+    private String content;
+    private Boolean isRead = false;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getUserId() { return userId; }
+    public void setUserId(Long userId) { this.userId = userId; }
+    public String getType() { return type; }
+    public void setType(String type) { this.type = type; }
+    public String getTitle() { return title; }
+    public void setTitle(String title) { this.title = title; }
+    public String getContent() { return content; }
+    public void setContent(String content) { this.content = content; }
+    public Boolean getIsRead() { return isRead; }
+    public void setIsRead(Boolean isRead) { this.isRead = isRead; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+    public LocalDateTime getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+}

+ 48 - 0
src/main/java/space/anyi/serve/entity/notification/NotificationVo.java

@@ -0,0 +1,48 @@
+package space.anyi.serve.entity.notification;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+@Schema(description = "通知视图")
+public class NotificationVo {
+    private String id;
+    private String type;
+    private String title;
+    private String content;
+    private Boolean isRead;
+    private LocalDateTime createdAt;
+
+    public static NotificationVo from(Notification n) {
+        if (n == null) return null;
+        NotificationVo vo = new NotificationVo();
+        vo.setId(n.getId().toString());
+        vo.setType(n.getType());
+        vo.setTitle(n.getTitle());
+        vo.setContent(n.getContent());
+        vo.setIsRead(n.getIsRead());
+        vo.setCreatedAt(n.getCreatedAt());
+        return vo;
+    }
+
+    public static List<NotificationVo> from(List<Notification> list) {
+        List<NotificationVo> res = new ArrayList<>();
+        for (Notification n : list) res.add(from(n));
+        return res;
+    }
+
+    public String getId() { return id; }
+    public void setId(String id) { this.id = id; }
+    public String getType() { return type; }
+    public void setType(String type) { this.type = type; }
+    public String getTitle() { return title; }
+    public void setTitle(String title) { this.title = title; }
+    public String getContent() { return content; }
+    public void setContent(String content) { this.content = content; }
+    public Boolean getIsRead() { return isRead; }
+    public void setIsRead(Boolean isRead) { this.isRead = isRead; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+}

+ 43 - 0
src/main/java/space/anyi/serve/entity/order/OrderTip.java

@@ -0,0 +1,43 @@
+package space.anyi.serve.entity.order;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Schema(description = "打赏订单")
+@TableName("dev.order_tip")
+public class OrderTip {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long userId;
+    private Long postId;
+    private Long expertId;
+    private BigDecimal amount;
+    private String status = "completed";
+    private LocalDateTime createTime;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getUserId() { return userId; }
+    public void setUserId(Long userId) { this.userId = userId; }
+    public Long getPostId() { return postId; }
+    public void setPostId(Long postId) { this.postId = postId; }
+    public Long getExpertId() { return expertId; }
+    public void setExpertId(Long expertId) { this.expertId = expertId; }
+    public BigDecimal getAmount() { return amount; }
+    public void setAmount(BigDecimal amount) { this.amount = amount; }
+    public String getStatus() { return status; }
+    public void setStatus(String status) { this.status = status; }
+    public LocalDateTime getCreateTime() { return createTime; }
+    public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+    public LocalDateTime getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+}

+ 59 - 0
src/main/java/space/anyi/serve/entity/order/OrderTipVo.java

@@ -0,0 +1,59 @@
+package space.anyi.serve.entity.order;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+@Schema(description = "打赏订单视图")
+public class OrderTipVo {
+    private String id;
+    private String userId;
+    private String postId;
+    private String postTitle;
+    private String expertId;
+    private String expertName;
+    private BigDecimal amount;
+    private String status;
+    private LocalDateTime createTime;
+
+    public static OrderTipVo from(OrderTip o) {
+        if (o == null) return null;
+        OrderTipVo vo = new OrderTipVo();
+        vo.setId(o.getId().toString());
+        vo.setUserId(o.getUserId().toString());
+        vo.setPostId(o.getPostId().toString());
+        vo.setExpertId(o.getExpertId().toString());
+        vo.setAmount(o.getAmount());
+        vo.setStatus(o.getStatus());
+        vo.setCreateTime(o.getCreateTime());
+        return vo;
+    }
+
+    public static List<OrderTipVo> from(List<OrderTip> list) {
+        List<OrderTipVo> res = new ArrayList<>();
+        for (OrderTip o : list) res.add(from(o));
+        return res;
+    }
+
+    public String getId() { return id; }
+    public void setId(String id) { this.id = id; }
+    public String getUserId() { return userId; }
+    public void setUserId(String userId) { this.userId = userId; }
+    public String getPostId() { return postId; }
+    public void setPostId(String postId) { this.postId = postId; }
+    public String getPostTitle() { return postTitle; }
+    public void setPostTitle(String postTitle) { this.postTitle = postTitle; }
+    public String getExpertId() { return expertId; }
+    public void setExpertId(String expertId) { this.expertId = expertId; }
+    public String getExpertName() { return expertName; }
+    public void setExpertName(String expertName) { this.expertName = expertName; }
+    public BigDecimal getAmount() { return amount; }
+    public void setAmount(BigDecimal amount) { this.amount = amount; }
+    public String getStatus() { return status; }
+    public void setStatus(String status) { this.status = status; }
+    public LocalDateTime getCreateTime() { return createTime; }
+    public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
+}

+ 55 - 0
src/main/java/space/anyi/serve/entity/post/Post.java

@@ -0,0 +1,55 @@
+package space.anyi.serve.entity.post;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Schema(description = "帖子")
+@TableName("dev.post")
+public class Post {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long expertId;
+    private String title;
+    private String contentIntro;
+    private String contentPaid;
+    private BigDecimal price;
+    private String hitStatus = "pending";
+    private Integer viewCount = 0;
+    private LocalDateTime publishTime;
+    private LocalDateTime expireTime;
+    private Integer deleteFlag = 0;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getExpertId() { return expertId; }
+    public void setExpertId(Long expertId) { this.expertId = expertId; }
+    public String getTitle() { return title; }
+    public void setTitle(String title) { this.title = title; }
+    public String getContentIntro() { return contentIntro; }
+    public void setContentIntro(String contentIntro) { this.contentIntro = contentIntro; }
+    public String getContentPaid() { return contentPaid; }
+    public void setContentPaid(String contentPaid) { this.contentPaid = contentPaid; }
+    public BigDecimal getPrice() { return price; }
+    public void setPrice(BigDecimal price) { this.price = price; }
+    public String getHitStatus() { return hitStatus; }
+    public void setHitStatus(String hitStatus) { this.hitStatus = hitStatus; }
+    public Integer getViewCount() { return viewCount; }
+    public void setViewCount(Integer viewCount) { this.viewCount = viewCount; }
+    public LocalDateTime getPublishTime() { return publishTime; }
+    public void setPublishTime(LocalDateTime publishTime) { this.publishTime = publishTime; }
+    public LocalDateTime getExpireTime() { return expireTime; }
+    public void setExpireTime(LocalDateTime expireTime) { this.expireTime = expireTime; }
+    public Integer getDeleteFlag() { return deleteFlag; }
+    public void setDeleteFlag(Integer deleteFlag) { this.deleteFlag = deleteFlag; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+    public LocalDateTime getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+}

+ 40 - 0
src/main/java/space/anyi/serve/entity/post/PostDto.java

@@ -0,0 +1,40 @@
+package space.anyi.serve.entity.post;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Schema(description = "帖子创建/编辑请求")
+public class PostDto {
+    @NotBlank(message = "标题不能为空")
+    @Schema(description = "帖子标题(含期号)")
+    private String title;
+
+    @Schema(description = "内容简介(公开)")
+    private String contentIntro;
+
+    @Schema(description = "付费内容")
+    private String contentPaid;
+
+    @NotNull(message = "价格不能为空")
+    @Schema(description = "打赏金额")
+    private BigDecimal price;
+
+    @NotNull(message = "过期时间不能为空")
+    @Schema(description = "过期时间,超出此时间自动转为公开")
+    private LocalDateTime expireTime;
+
+    public String getTitle() { return title; }
+    public void setTitle(String title) { this.title = title; }
+    public String getContentIntro() { return contentIntro; }
+    public void setContentIntro(String contentIntro) { this.contentIntro = contentIntro; }
+    public String getContentPaid() { return contentPaid; }
+    public void setContentPaid(String contentPaid) { this.contentPaid = contentPaid; }
+    public BigDecimal getPrice() { return price; }
+    public void setPrice(BigDecimal price) { this.price = price; }
+    public LocalDateTime getExpireTime() { return expireTime; }
+    public void setExpireTime(LocalDateTime expireTime) { this.expireTime = expireTime; }
+}

+ 33 - 0
src/main/java/space/anyi/serve/entity/post/PostViewRecord.java

@@ -0,0 +1,33 @@
+package space.anyi.serve.entity.post;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "帖子访问记录")
+@TableName("dev.post_view_record")
+public class PostViewRecord {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long postId;
+    private Long userId;
+    private LocalDateTime viewTime;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getPostId() { return postId; }
+    public void setPostId(Long postId) { this.postId = postId; }
+    public Long getUserId() { return userId; }
+    public void setUserId(Long userId) { this.userId = userId; }
+    public LocalDateTime getViewTime() { return viewTime; }
+    public void setViewTime(LocalDateTime viewTime) { this.viewTime = viewTime; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+    public LocalDateTime getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+}

+ 78 - 0
src/main/java/space/anyi/serve/entity/post/PostVo.java

@@ -0,0 +1,78 @@
+package space.anyi.serve.entity.post;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+@Schema(description = "帖子响应视图")
+public class PostVo {
+    private String id;
+    private String expertId;
+    private String expertName;
+    private String expertAvatar;
+    private String title;
+    private String contentIntro;
+    private String contentPaid;
+    private BigDecimal price;
+    private String hitStatus;
+    private Integer viewCount;
+    private Boolean isPublic;
+    private Boolean isPaid;
+    private LocalDateTime publishTime;
+    private LocalDateTime expireTime;
+
+    public static PostVo from(Post post) {
+        if (post == null) return null;
+        PostVo vo = new PostVo();
+        vo.setId(post.getId().toString());
+        vo.setExpertId(post.getExpertId().toString());
+        vo.setTitle(post.getTitle());
+        vo.setContentIntro(post.getContentIntro());
+        vo.setContentPaid(post.getContentPaid());
+        vo.setPrice(post.getPrice());
+        vo.setHitStatus(post.getHitStatus());
+        vo.setViewCount(post.getViewCount());
+        vo.setPublishTime(post.getPublishTime());
+        vo.setExpireTime(post.getExpireTime());
+        vo.setIsPublic(LocalDateTime.now().isAfter(post.getExpireTime()));
+        return vo;
+    }
+
+    public static List<PostVo> from(List<Post> list) {
+        List<PostVo> res = new ArrayList<>();
+        for (Post p : list) res.add(from(p));
+        return res;
+    }
+
+    public String getId() { return id; }
+    public void setId(String id) { this.id = id; }
+    public String getExpertId() { return expertId; }
+    public void setExpertId(String expertId) { this.expertId = expertId; }
+    public String getExpertName() { return expertName; }
+    public void setExpertName(String expertName) { this.expertName = expertName; }
+    public String getExpertAvatar() { return expertAvatar; }
+    public void setExpertAvatar(String expertAvatar) { this.expertAvatar = expertAvatar; }
+    public String getTitle() { return title; }
+    public void setTitle(String title) { this.title = title; }
+    public String getContentIntro() { return contentIntro; }
+    public void setContentIntro(String contentIntro) { this.contentIntro = contentIntro; }
+    public String getContentPaid() { return contentPaid; }
+    public void setContentPaid(String contentPaid) { this.contentPaid = contentPaid; }
+    public BigDecimal getPrice() { return price; }
+    public void setPrice(BigDecimal price) { this.price = price; }
+    public String getHitStatus() { return hitStatus; }
+    public void setHitStatus(String hitStatus) { this.hitStatus = hitStatus; }
+    public Integer getViewCount() { return viewCount; }
+    public void setViewCount(Integer viewCount) { this.viewCount = viewCount; }
+    public Boolean getIsPublic() { return isPublic; }
+    public void setIsPublic(Boolean isPublic) { this.isPublic = isPublic; }
+    public Boolean getIsPaid() { return isPaid; }
+    public void setIsPaid(Boolean isPaid) { this.isPaid = isPaid; }
+    public LocalDateTime getPublishTime() { return publishTime; }
+    public void setPublishTime(LocalDateTime publishTime) { this.publishTime = publishTime; }
+    public LocalDateTime getExpireTime() { return expireTime; }
+    public void setExpireTime(LocalDateTime expireTime) { this.expireTime = expireTime; }
+}

+ 33 - 0
src/main/java/space/anyi/serve/entity/profile/UserProfile.java

@@ -0,0 +1,33 @@
+package space.anyi.serve.entity.profile;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "用户扩展信息")
+@TableName("dev.user_profile")
+public class UserProfile {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long userId;
+    private String level = "gold";
+    private Boolean isRealname = false;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getUserId() { return userId; }
+    public void setUserId(Long userId) { this.userId = userId; }
+    public String getLevel() { return level; }
+    public void setLevel(String level) { this.level = level; }
+    public Boolean getIsRealname() { return isRealname; }
+    public void setIsRealname(Boolean isRealname) { this.isRealname = isRealname; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+    public LocalDateTime getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+}

+ 22 - 0
src/main/java/space/anyi/serve/entity/profile/UserProfileVo.java

@@ -0,0 +1,22 @@
+package space.anyi.serve.entity.profile;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(description = "用户扩展信息视图")
+public class UserProfileVo {
+    private String level;
+    private Boolean isRealname;
+
+    public static UserProfileVo from(UserProfile p) {
+        if (p == null) return null;
+        UserProfileVo vo = new UserProfileVo();
+        vo.setLevel(p.getLevel());
+        vo.setIsRealname(p.getIsRealname());
+        return vo;
+    }
+
+    public String getLevel() { return level; }
+    public void setLevel(String level) { this.level = level; }
+    public Boolean getIsRealname() { return isRealname; }
+    public void setIsRealname(Boolean isRealname) { this.isRealname = isRealname; }
+}

+ 48 - 0
src/main/java/space/anyi/serve/entity/realname/RealnameAuth.java

@@ -0,0 +1,48 @@
+package space.anyi.serve.entity.realname;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "实名认证")
+@TableName("dev.realname_auth")
+public class RealnameAuth {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long userId;
+    private String realName;
+    private String idCard;
+    private String idCardFront;
+    private String idCardBack;
+    private String status = "pending";
+    private String rejectReason;
+    private LocalDateTime reviewTime;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getUserId() { return userId; }
+    public void setUserId(Long userId) { this.userId = userId; }
+    public String getRealName() { return realName; }
+    public void setRealName(String realName) { this.realName = realName; }
+    public String getIdCard() { return idCard; }
+    public void setIdCard(String idCard) { this.idCard = idCard; }
+    public String getIdCardFront() { return idCardFront; }
+    public void setIdCardFront(String idCardFront) { this.idCardFront = idCardFront; }
+    public String getIdCardBack() { return idCardBack; }
+    public void setIdCardBack(String idCardBack) { this.idCardBack = idCardBack; }
+    public String getStatus() { return status; }
+    public void setStatus(String status) { this.status = status; }
+    public String getRejectReason() { return rejectReason; }
+    public void setRejectReason(String rejectReason) { this.rejectReason = rejectReason; }
+    public LocalDateTime getReviewTime() { return reviewTime; }
+    public void setReviewTime(LocalDateTime reviewTime) { this.reviewTime = reviewTime; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+    public LocalDateTime getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+}

+ 60 - 0
src/main/java/space/anyi/serve/entity/realname/RealnameAuthVo.java

@@ -0,0 +1,60 @@
+package space.anyi.serve.entity.realname;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+@Schema(description = "实名认证视图")
+public class RealnameAuthVo {
+    private String id;
+    private String userId;
+    private String realName;
+    private String idCard;
+    private String idCardFront;
+    private String idCardBack;
+    private String status;
+    private String rejectReason;
+    private LocalDateTime createdAt;
+
+    public static RealnameAuthVo from(RealnameAuth r) {
+        if (r == null) return null;
+        RealnameAuthVo vo = new RealnameAuthVo();
+        vo.setId(r.getId().toString());
+        vo.setUserId(r.getUserId().toString());
+        vo.setRealName(r.getRealName());
+        vo.setIdCard(r.getIdCard());
+        vo.setIdCardFront(r.getIdCardFront());
+        vo.setIdCardBack(r.getIdCardBack());
+        vo.setStatus(r.getStatus());
+        vo.setRejectReason(r.getRejectReason());
+        vo.setCreatedAt(r.getCreatedAt());
+        return vo;
+    }
+
+    public static List<RealnameAuthVo> from(List<RealnameAuth> list) {
+        List<RealnameAuthVo> res = new ArrayList<>();
+        for (RealnameAuth r : list) res.add(from(r));
+        return res;
+    }
+
+    public String getId() { return id; }
+    public void setId(String id) { this.id = id; }
+    public String getUserId() { return userId; }
+    public void setUserId(String userId) { this.userId = userId; }
+    public String getRealName() { return realName; }
+    public void setRealName(String realName) { this.realName = realName; }
+    public String getIdCard() { return idCard; }
+    public void setIdCard(String idCard) { this.idCard = idCard; }
+    public String getIdCardFront() { return idCardFront; }
+    public void setIdCardFront(String idCardFront) { this.idCardFront = idCardFront; }
+    public String getIdCardBack() { return idCardBack; }
+    public void setIdCardBack(String idCardBack) { this.idCardBack = idCardBack; }
+    public String getStatus() { return status; }
+    public void setStatus(String status) { this.status = status; }
+    public String getRejectReason() { return rejectReason; }
+    public void setRejectReason(String rejectReason) { this.rejectReason = rejectReason; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+}

+ 31 - 0
src/main/java/space/anyi/serve/entity/wallet/Wallet.java

@@ -0,0 +1,31 @@
+package space.anyi.serve.entity.wallet;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Schema(description = "钱包")
+@TableName("dev.wallet")
+public class Wallet {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long userId;
+    private BigDecimal balance = BigDecimal.ZERO;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getUserId() { return userId; }
+    public void setUserId(Long userId) { this.userId = userId; }
+    public BigDecimal getBalance() { return balance; }
+    public void setBalance(BigDecimal balance) { this.balance = balance; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+    public LocalDateTime getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+}

+ 49 - 0
src/main/java/space/anyi/serve/entity/wallet/WalletTransaction.java

@@ -0,0 +1,49 @@
+package space.anyi.serve.entity.wallet;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Schema(description = "钱包流水")
+@TableName("dev.wallet_transaction")
+public class WalletTransaction {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long userId;
+    private String type;
+    private BigDecimal amount;
+    private BigDecimal balanceBefore;
+    private BigDecimal balanceAfter;
+    private String status = "success";
+    private String remark;
+    private LocalDateTime reviewTime;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public Long getUserId() { return userId; }
+    public void setUserId(Long userId) { this.userId = userId; }
+    public String getType() { return type; }
+    public void setType(String type) { this.type = type; }
+    public BigDecimal getAmount() { return amount; }
+    public void setAmount(BigDecimal amount) { this.amount = amount; }
+    public BigDecimal getBalanceBefore() { return balanceBefore; }
+    public void setBalanceBefore(BigDecimal balanceBefore) { this.balanceBefore = balanceBefore; }
+    public BigDecimal getBalanceAfter() { return balanceAfter; }
+    public void setBalanceAfter(BigDecimal balanceAfter) { this.balanceAfter = balanceAfter; }
+    public String getStatus() { return status; }
+    public void setStatus(String status) { this.status = status; }
+    public String getRemark() { return remark; }
+    public void setRemark(String remark) { this.remark = remark; }
+    public LocalDateTime getReviewTime() { return reviewTime; }
+    public void setReviewTime(LocalDateTime reviewTime) { this.reviewTime = reviewTime; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+    public LocalDateTime getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
+}

+ 57 - 0
src/main/java/space/anyi/serve/entity/wallet/WalletTransactionVo.java

@@ -0,0 +1,57 @@
+package space.anyi.serve.entity.wallet;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+@Schema(description = "钱包流水视图")
+public class WalletTransactionVo {
+    private String id;
+    private String type;
+    private BigDecimal amount;
+    private BigDecimal balanceBefore;
+    private BigDecimal balanceAfter;
+    private String status;
+    private String remark;
+    private LocalDateTime createdAt;
+
+    public static WalletTransactionVo from(WalletTransaction t) {
+        if (t == null) return null;
+        WalletTransactionVo vo = new WalletTransactionVo();
+        vo.setId(t.getId().toString());
+        vo.setType(t.getType());
+        vo.setAmount(t.getAmount());
+        vo.setBalanceBefore(t.getBalanceBefore());
+        vo.setBalanceAfter(t.getBalanceAfter());
+        vo.setStatus(t.getStatus());
+        vo.setRemark(t.getRemark());
+        vo.setCreatedAt(t.getCreatedAt());
+        return vo;
+    }
+
+    public static List<WalletTransactionVo> from(List<WalletTransaction> list) {
+        List<WalletTransactionVo> res = new ArrayList<>();
+        for (WalletTransaction t : list) res.add(from(t));
+        return res;
+    }
+
+    public String getId() { return id; }
+    public void setId(String id) { this.id = id; }
+    public String getType() { return type; }
+    public void setType(String type) { this.type = type; }
+    public BigDecimal getAmount() { return amount; }
+    public void setAmount(BigDecimal amount) { this.amount = amount; }
+    public BigDecimal getBalanceBefore() { return balanceBefore; }
+    public void setBalanceBefore(BigDecimal balanceBefore) { this.balanceBefore = balanceBefore; }
+    public BigDecimal getBalanceAfter() { return balanceAfter; }
+    public void setBalanceAfter(BigDecimal balanceAfter) { this.balanceAfter = balanceAfter; }
+    public String getStatus() { return status; }
+    public void setStatus(String status) { this.status = status; }
+    public String getRemark() { return remark; }
+    public void setRemark(String remark) { this.remark = remark; }
+    public LocalDateTime getCreatedAt() { return createdAt; }
+    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
+}

+ 9 - 0
src/main/java/space/anyi/serve/mapper/AttachmentMapper.java

@@ -0,0 +1,9 @@
+package space.anyi.serve.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import space.anyi.serve.entity.attachment.Attachment;
+
+@Mapper
+public interface AttachmentMapper extends BaseMapper<Attachment> {
+}

+ 9 - 0
src/main/java/space/anyi/serve/mapper/NotificationMapper.java

@@ -0,0 +1,9 @@
+package space.anyi.serve.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import space.anyi.serve.entity.notification.Notification;
+
+@Mapper
+public interface NotificationMapper extends BaseMapper<Notification> {
+}

+ 9 - 0
src/main/java/space/anyi/serve/mapper/OrderTipMapper.java

@@ -0,0 +1,9 @@
+package space.anyi.serve.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import space.anyi.serve.entity.order.OrderTip;
+
+@Mapper
+public interface OrderTipMapper extends BaseMapper<OrderTip> {
+}

+ 9 - 0
src/main/java/space/anyi/serve/mapper/PostMapper.java

@@ -0,0 +1,9 @@
+package space.anyi.serve.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import space.anyi.serve.entity.post.Post;
+
+@Mapper
+public interface PostMapper extends BaseMapper<Post> {
+}

+ 9 - 0
src/main/java/space/anyi/serve/mapper/PostViewRecordMapper.java

@@ -0,0 +1,9 @@
+package space.anyi.serve.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import space.anyi.serve.entity.post.PostViewRecord;
+
+@Mapper
+public interface PostViewRecordMapper extends BaseMapper<PostViewRecord> {
+}

+ 9 - 0
src/main/java/space/anyi/serve/mapper/RealnameAuthMapper.java

@@ -0,0 +1,9 @@
+package space.anyi.serve.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import space.anyi.serve.entity.realname.RealnameAuth;
+
+@Mapper
+public interface RealnameAuthMapper extends BaseMapper<RealnameAuth> {
+}

+ 9 - 0
src/main/java/space/anyi/serve/mapper/UserProfileMapper.java

@@ -0,0 +1,9 @@
+package space.anyi.serve.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import space.anyi.serve.entity.profile.UserProfile;
+
+@Mapper
+public interface UserProfileMapper extends BaseMapper<UserProfile> {
+}

+ 9 - 0
src/main/java/space/anyi/serve/mapper/WalletMapper.java

@@ -0,0 +1,9 @@
+package space.anyi.serve.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import space.anyi.serve.entity.wallet.Wallet;
+
+@Mapper
+public interface WalletMapper extends BaseMapper<Wallet> {
+}

+ 9 - 0
src/main/java/space/anyi/serve/mapper/WalletTransactionMapper.java

@@ -0,0 +1,9 @@
+package space.anyi.serve.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import space.anyi.serve.entity.wallet.WalletTransaction;
+
+@Mapper
+public interface WalletTransactionMapper extends BaseMapper<WalletTransaction> {
+}

+ 8 - 0
src/main/java/space/anyi/serve/service/AttachmentService.java

@@ -0,0 +1,8 @@
+package space.anyi.serve.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import space.anyi.serve.entity.attachment.Attachment;
+
+public interface AttachmentService extends IService<Attachment> {
+    Attachment upload(Long userId, String url, String type);
+}

+ 2 - 0
src/main/java/space/anyi/serve/service/MetaService.java

@@ -14,4 +14,6 @@ public interface MetaService extends IService<Meta> {
     Meta getMeta(String key);
 
     Boolean updateMeta(String websiteMetaKey, Meta meta);
+
+    Boolean updateMetaValue(String key, Object value);
 }

+ 14 - 0
src/main/java/space/anyi/serve/service/NotificationService.java

@@ -0,0 +1,14 @@
+package space.anyi.serve.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import space.anyi.serve.entity.notification.Notification;
+import space.anyi.serve.entity.notification.NotificationVo;
+
+import java.util.List;
+
+public interface NotificationService extends IService<Notification> {
+    List<NotificationVo> listNotifications(Long userId);
+    long getUnreadCount(Long userId);
+    void markRead(Long id, Long userId);
+    void createNotification(Long userId, String type, String title, String content);
+}

+ 16 - 0
src/main/java/space/anyi/serve/service/PostService.java

@@ -0,0 +1,16 @@
+package space.anyi.serve.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import space.anyi.serve.entity.post.Post;
+import space.anyi.serve.entity.post.PostVo;
+
+public interface PostService extends IService<Post> {
+    Page<PostVo> listPosts(String keyword, String status, int pageNum, int pageSize, Long currentUserId);
+    PostVo getPostDetail(Long id, Long currentUserId);
+    Long createPost(Post post);
+    void updateHitStatus(Long id, String hitStatus);
+    void updateViewCount(Long id, Integer viewCount);
+    boolean hasUserPaid(Long postId, Long userId);
+    Page<Post> listExpertPreviousPosts(Long expertId, int pageNum, int pageSize);
+}

+ 10 - 0
src/main/java/space/anyi/serve/service/RealnameAuthService.java

@@ -0,0 +1,10 @@
+package space.anyi.serve.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import space.anyi.serve.entity.realname.RealnameAuth;
+
+public interface RealnameAuthService extends IService<RealnameAuth> {
+    RealnameAuth getByUserId(Long userId);
+    void submit(Long userId, String realName, String idCard, String idCardFront, String idCardBack);
+    void review(Long id, boolean approved, String rejectReason);
+}

+ 11 - 0
src/main/java/space/anyi/serve/service/TipOrderService.java

@@ -0,0 +1,11 @@
+package space.anyi.serve.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import space.anyi.serve.entity.order.OrderTip;
+import space.anyi.serve.entity.order.OrderTipVo;
+
+public interface TipOrderService extends IService<OrderTip> {
+    OrderTip createAndPay(Long userId, Long postId, String requestId);
+    Page<OrderTipVo> listOrders(Long userId, String status, int pageNum, int pageSize);
+}

+ 9 - 0
src/main/java/space/anyi/serve/service/UserProfileService.java

@@ -0,0 +1,9 @@
+package space.anyi.serve.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import space.anyi.serve.entity.profile.UserProfile;
+
+public interface UserProfileService extends IService<UserProfile> {
+    UserProfile getByUserId(Long userId);
+    UserProfile createProfile(Long userId);
+}

+ 18 - 0
src/main/java/space/anyi/serve/service/WalletService.java

@@ -0,0 +1,18 @@
+package space.anyi.serve.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import space.anyi.serve.entity.wallet.Wallet;
+import space.anyi.serve.entity.wallet.WalletTransaction;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+public interface WalletService extends IService<Wallet> {
+    Wallet getByUserId(Long userId);
+    Wallet createWallet(Long userId);
+    void deductBalance(Long userId, BigDecimal amount, Long postId, Long expertId, String requestId);
+    void addBalance(Long userId, BigDecimal amount, String type, String remark);
+    List<WalletTransaction> getTransactions(Long userId, int page, int size);
+    WalletTransaction applyWithdraw(Long userId, BigDecimal amount);
+    void reviewWithdraw(Long transactionId, boolean approved, String rejectReason);
+}

+ 25 - 0
src/main/java/space/anyi/serve/service/impl/AttachmentServiceImpl.java

@@ -0,0 +1,25 @@
+package space.anyi.serve.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+import space.anyi.serve.entity.attachment.Attachment;
+import space.anyi.serve.mapper.AttachmentMapper;
+import space.anyi.serve.service.AttachmentService;
+
+import java.time.LocalDateTime;
+
+@Service
+public class AttachmentServiceImpl extends ServiceImpl<AttachmentMapper, Attachment> implements AttachmentService {
+
+    @Override
+    public Attachment upload(Long userId, String url, String type) {
+        Attachment attachment = new Attachment();
+        attachment.setUserId(userId);
+        attachment.setUrl(url);
+        attachment.setType(type);
+        attachment.setCreatedAt(LocalDateTime.now());
+        attachment.setUpdatedAt(LocalDateTime.now());
+        save(attachment);
+        return attachment;
+    }
+}

+ 14 - 0
src/main/java/space/anyi/serve/service/impl/MetaServiceImpl.java

@@ -42,6 +42,20 @@ public class MetaServiceImpl extends ServiceImpl<MetaMapper, Meta>
         }
     }
 
+    @Override
+    public Boolean updateMetaValue(String key, Object value) {
+        try {
+            String json = objectMapper.writeValueAsString(value);
+            UpdateWrapper<Meta> updateWrapper = new UpdateWrapper<Meta>()
+                    .eq("key", key)
+                    .set("value", json);
+            return update(updateWrapper);
+        } catch (JsonProcessingException e) {
+            log.error("序列化JSON失败", e);
+            throw new RuntimeException(e);
+        }
+    }
+
     @Override
     public Meta getMeta(String key) {
         LambdaQueryWrapper<Meta> queryWrapper = new LambdaQueryWrapper<Meta>()

+ 59 - 0
src/main/java/space/anyi/serve/service/impl/NotificationServiceImpl.java

@@ -0,0 +1,59 @@
+package space.anyi.serve.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import space.anyi.serve.entity.notification.Notification;
+import space.anyi.serve.entity.notification.NotificationVo;
+import space.anyi.serve.mapper.NotificationMapper;
+import space.anyi.serve.service.NotificationService;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Service
+public class NotificationServiceImpl extends ServiceImpl<NotificationMapper, Notification> implements NotificationService {
+
+    @Override
+    public List<NotificationVo> listNotifications(Long userId) {
+        List<Notification> list = lambdaQuery()
+                .eq(Notification::getUserId, userId)
+                .orderByDesc(Notification::getCreatedAt)
+                .list();
+        return NotificationVo.from(list);
+    }
+
+    @Override
+    public long getUnreadCount(Long userId) {
+        return lambdaQuery()
+                .eq(Notification::getUserId, userId)
+                .eq(Notification::getIsRead, false)
+                .count();
+    }
+
+    @Override
+    @Transactional
+    public void markRead(Long id, Long userId) {
+        Notification n = getById(id);
+        if (n != null && n.getUserId().equals(userId)) {
+            n.setIsRead(true);
+            n.setUpdatedAt(LocalDateTime.now());
+            updateById(n);
+        }
+    }
+
+    @Override
+    @Transactional
+    public void createNotification(Long userId, String type, String title, String content) {
+        Notification n = new Notification();
+        n.setUserId(userId);
+        n.setType(type);
+        n.setTitle(title);
+        n.setContent(content);
+        n.setIsRead(false);
+        n.setCreatedAt(LocalDateTime.now());
+        n.setUpdatedAt(LocalDateTime.now());
+        save(n);
+    }
+}

+ 175 - 0
src/main/java/space/anyi/serve/service/impl/PostServiceImpl.java

@@ -0,0 +1,175 @@
+package space.anyi.serve.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import space.anyi.serve.entity.order.OrderTip;
+import space.anyi.serve.entity.post.Post;
+import space.anyi.serve.entity.post.PostVo;
+import space.anyi.serve.entity.user.User;
+import space.anyi.serve.mapper.OrderTipMapper;
+import space.anyi.serve.mapper.PostMapper;
+import space.anyi.serve.mapper.PostViewRecordMapper;
+import space.anyi.serve.entity.post.PostViewRecord;
+import space.anyi.serve.service.PostService;
+import space.anyi.serve.service.UserService;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements PostService {
+
+    private final PostViewRecordMapper postViewRecordMapper;
+    private final OrderTipMapper orderTipMapper;
+    private final UserService userService;
+
+    public PostServiceImpl(PostViewRecordMapper postViewRecordMapper,
+                           OrderTipMapper orderTipMapper,
+                           UserService userService) {
+        this.postViewRecordMapper = postViewRecordMapper;
+        this.orderTipMapper = orderTipMapper;
+        this.userService = userService;
+    }
+
+    @Override
+    public Page<PostVo> listPosts(String keyword, String status, int pageNum, int pageSize, Long currentUserId) {
+        Page<Post> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<Post> wrapper = new LambdaQueryWrapper<Post>()
+                .eq(Post::getDeleteFlag, 0);
+
+        if (keyword != null && !keyword.isEmpty()) {
+            wrapper.like(Post::getTitle, keyword);
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        if ("on_sale".equals(status)) {
+            wrapper.gt(Post::getExpireTime, now);
+        } else if ("public".equals(status)) {
+            wrapper.le(Post::getExpireTime, now);
+        }
+
+        wrapper.orderByDesc(Post::getPublishTime);
+        Page<Post> postPage = page(page, wrapper);
+
+        List<PostVo> vos = postPage.getRecords().stream().map(post -> {
+            PostVo vo = PostVo.from(post);
+            User expert = userService.queryById(post.getExpertId());
+            if (expert != null) {
+                vo.setExpertName(expert.getUsername());
+                vo.setExpertAvatar(expert.getAvatar());
+            }
+            if (currentUserId != null) {
+                vo.setIsPaid(hasUserPaid(post.getId(), currentUserId));
+            }
+            return vo;
+        }).collect(Collectors.toList());
+
+        Page<PostVo> result = new Page<>(postPage.getCurrent(), postPage.getSize(), postPage.getTotal());
+        result.setRecords(vos);
+
+        if (currentUserId != null) {
+            recordView(currentUserId, postPage.getRecords());
+        }
+
+        return result;
+    }
+
+    @Override
+    public PostVo getPostDetail(Long id, Long currentUserId) {
+        Post post = getById(id);
+        if (post == null || post.getDeleteFlag() == 1) return null;
+
+        PostVo vo = PostVo.from(post);
+        User expert = userService.queryById(post.getExpertId());
+        if (expert != null) {
+            vo.setExpertName(expert.getUsername());
+            vo.setExpertAvatar(expert.getAvatar());
+        }
+
+        boolean isExpired = LocalDateTime.now().isAfter(post.getExpireTime());
+        boolean isPaid = currentUserId != null && hasUserPaid(id, currentUserId);
+
+        if (!isExpired && !isPaid) {
+            vo.setContentPaid(null);
+        }
+        vo.setIsPaid(isPaid || isExpired);
+        return vo;
+    }
+
+    @Override
+    @Transactional
+    public Long createPost(Post post) {
+        post.setPublishTime(LocalDateTime.now());
+        post.setCreatedAt(LocalDateTime.now());
+        post.setUpdatedAt(LocalDateTime.now());
+        save(post);
+        return post.getId();
+    }
+
+    @Override
+    @Transactional
+    public void updateHitStatus(Long id, String hitStatus) {
+        Post post = getById(id);
+        if (post == null) throw new IllegalArgumentException("帖子不存在");
+        post.setHitStatus(hitStatus);
+        post.setUpdatedAt(LocalDateTime.now());
+        updateById(post);
+    }
+
+    @Override
+    @Transactional
+    public void updateViewCount(Long id, Integer viewCount) {
+        Post post = getById(id);
+        if (post == null) throw new IllegalArgumentException("帖子不存在");
+        post.setViewCount(viewCount);
+        post.setUpdatedAt(LocalDateTime.now());
+        updateById(post);
+    }
+
+    @Override
+    public boolean hasUserPaid(Long postId, Long userId) {
+        if (userId == null) return false;
+        Long count = orderTipMapper.selectCount(
+                new LambdaQueryWrapper<OrderTip>()
+                        .eq(OrderTip::getPostId, postId)
+                        .eq(OrderTip::getUserId, userId)
+        );
+        return count > 0;
+    }
+
+    @Override
+    public Page<Post> listExpertPreviousPosts(Long expertId, int pageNum, int pageSize) {
+        Page<Post> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<Post> wrapper = new LambdaQueryWrapper<Post>()
+                .eq(Post::getExpertId, expertId)
+                .le(Post::getExpireTime, LocalDateTime.now())
+                .eq(Post::getDeleteFlag, 0)
+                .orderByDesc(Post::getPublishTime);
+        return page(page, wrapper);
+    }
+
+    private void recordView(Long userId, List<Post> posts) {
+        for (Post post : posts) {
+            Long count = postViewRecordMapper.selectCount(
+                    new LambdaQueryWrapper<PostViewRecord>()
+                            .eq(PostViewRecord::getPostId, post.getId())
+                            .eq(PostViewRecord::getUserId, userId)
+            );
+            if (count == 0) {
+                PostViewRecord record = new PostViewRecord();
+                record.setPostId(post.getId());
+                record.setUserId(userId);
+                record.setViewTime(LocalDateTime.now());
+                record.setCreatedAt(LocalDateTime.now());
+                record.setUpdatedAt(LocalDateTime.now());
+                postViewRecordMapper.insert(record);
+                post.setViewCount(post.getViewCount() + 1);
+                updateById(post);
+            }
+        }
+    }
+}

+ 78 - 0
src/main/java/space/anyi/serve/service/impl/RealnameAuthServiceImpl.java

@@ -0,0 +1,78 @@
+package space.anyi.serve.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import space.anyi.serve.entity.profile.UserProfile;
+import space.anyi.serve.entity.realname.RealnameAuth;
+import space.anyi.serve.mapper.RealnameAuthMapper;
+import space.anyi.serve.mapper.UserProfileMapper;
+import space.anyi.serve.service.NotificationService;
+import space.anyi.serve.service.RealnameAuthService;
+
+import java.time.LocalDateTime;
+
+@Service
+public class RealnameAuthServiceImpl extends ServiceImpl<RealnameAuthMapper, RealnameAuth> implements RealnameAuthService {
+
+    private final UserProfileMapper userProfileMapper;
+    private final NotificationService notificationService;
+
+    public RealnameAuthServiceImpl(UserProfileMapper userProfileMapper, NotificationService notificationService) {
+        this.userProfileMapper = userProfileMapper;
+        this.notificationService = notificationService;
+    }
+
+    @Override
+    public RealnameAuth getByUserId(Long userId) {
+        return getOne(new LambdaQueryWrapper<RealnameAuth>().eq(RealnameAuth::getUserId, userId));
+    }
+
+    @Override
+    @Transactional
+    public void submit(Long userId, String realName, String idCard, String idCardFront, String idCardBack) {
+        RealnameAuth existing = getByUserId(userId);
+        if (existing != null) throw new IllegalArgumentException("已提交过实名认证");
+
+        RealnameAuth auth = new RealnameAuth();
+        auth.setUserId(userId);
+        auth.setRealName(realName);
+        auth.setIdCard(idCard);
+        auth.setIdCardFront(idCardFront);
+        auth.setIdCardBack(idCardBack);
+        auth.setStatus("pending");
+        auth.setCreatedAt(LocalDateTime.now());
+        auth.setUpdatedAt(LocalDateTime.now());
+        save(auth);
+    }
+
+    @Override
+    @Transactional
+    public void review(Long id, boolean approved, String rejectReason) {
+        RealnameAuth auth = getById(id);
+        if (auth == null) throw new IllegalArgumentException("认证记录不存在");
+        if (!"pending".equals(auth.getStatus())) throw new IllegalArgumentException("已审核");
+
+        if (approved) {
+            auth.setStatus("approved");
+            UserProfile profile = userProfileMapper.selectOne(
+                    new LambdaQueryWrapper<UserProfile>().eq(UserProfile::getUserId, auth.getUserId()));
+            if (profile != null) {
+                profile.setIsRealname(true);
+                profile.setUpdatedAt(LocalDateTime.now());
+                userProfileMapper.updateById(profile);
+            }
+            notificationService.createNotification(auth.getUserId(), "system", "实名认证通过",
+                    "您的实名认证已通过");
+        } else {
+            auth.setStatus("rejected");
+            auth.setRejectReason(rejectReason);
+            notificationService.createNotification(auth.getUserId(), "system", "实名认证驳回",
+                    "实名认证被驳回" + (rejectReason != null ? ",原因:" + rejectReason : ""));
+        }
+        auth.setReviewTime(LocalDateTime.now());
+        auth.setUpdatedAt(LocalDateTime.now());
+        updateById(auth);
+    }
+}

+ 87 - 0
src/main/java/space/anyi/serve/service/impl/TipOrderServiceImpl.java

@@ -0,0 +1,87 @@
+package space.anyi.serve.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import space.anyi.serve.entity.order.OrderTip;
+import space.anyi.serve.entity.order.OrderTipVo;
+import space.anyi.serve.entity.post.Post;
+import space.anyi.serve.entity.user.User;
+import space.anyi.serve.mapper.OrderTipMapper;
+import space.anyi.serve.service.PostService;
+import space.anyi.serve.service.TipOrderService;
+import space.anyi.serve.service.WalletService;
+import space.anyi.serve.service.UserService;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class TipOrderServiceImpl extends ServiceImpl<OrderTipMapper, OrderTip> implements TipOrderService {
+
+    private final PostService postService;
+    private final WalletService walletService;
+    private final UserService userService;
+
+    public TipOrderServiceImpl(PostService postService, WalletService walletService, UserService userService) {
+        this.postService = postService;
+        this.walletService = walletService;
+        this.userService = userService;
+    }
+
+    @Override
+    @Transactional
+    public OrderTip createAndPay(Long userId, Long postId, String requestId) {
+        Post post = postService.getById(postId);
+        if (post == null || post.getDeleteFlag() == 1) throw new IllegalArgumentException("帖子不存在");
+        if (LocalDateTime.now().isAfter(post.getExpireTime())) throw new IllegalArgumentException("帖子已过期");
+
+        walletService.deductBalance(userId, post.getPrice(), postId, post.getExpertId(), requestId);
+
+        OrderTip order = new OrderTip();
+        order.setUserId(userId);
+        order.setPostId(postId);
+        order.setExpertId(post.getExpertId());
+        order.setAmount(post.getPrice());
+        order.setStatus("completed");
+        order.setCreateTime(LocalDateTime.now());
+        order.setCreatedAt(LocalDateTime.now());
+        order.setUpdatedAt(LocalDateTime.now());
+        save(order);
+        return order;
+    }
+
+    @Override
+    public Page<OrderTipVo> listOrders(Long userId, String status, int pageNum, int pageSize) {
+        Page<OrderTip> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<OrderTip> wrapper = new LambdaQueryWrapper<OrderTip>()
+                .eq(OrderTip::getUserId, userId);
+
+        if (status != null && !"all".equals(status)) {
+            wrapper.eq(OrderTip::getStatus, status);
+        }
+
+        wrapper.orderByDesc(OrderTip::getCreateTime);
+        Page<OrderTip> orderPage = page(page, wrapper);
+
+        List<OrderTipVo> vos = orderPage.getRecords().stream().map(order -> {
+            OrderTipVo vo = OrderTipVo.from(order);
+            Post post = postService.getById(order.getPostId());
+            if (post != null) {
+                vo.setPostTitle(post.getTitle());
+            }
+            User expert = userService.queryById(order.getExpertId());
+            if (expert != null) {
+                vo.setExpertName(expert.getUsername());
+            }
+            return vo;
+        }).collect(Collectors.toList());
+
+        Page<OrderTipVo> result = new Page<>(orderPage.getCurrent(), orderPage.getSize(), orderPage.getTotal());
+        result.setRecords(vos);
+        return result;
+    }
+}

+ 31 - 0
src/main/java/space/anyi/serve/service/impl/UserProfileServiceImpl.java

@@ -0,0 +1,31 @@
+package space.anyi.serve.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+import space.anyi.serve.entity.profile.UserProfile;
+import space.anyi.serve.mapper.UserProfileMapper;
+import space.anyi.serve.service.UserProfileService;
+
+import java.time.LocalDateTime;
+
+@Service
+public class UserProfileServiceImpl extends ServiceImpl<UserProfileMapper, UserProfile> implements UserProfileService {
+
+    @Override
+    public UserProfile getByUserId(Long userId) {
+        return getOne(new LambdaQueryWrapper<UserProfile>().eq(UserProfile::getUserId, userId));
+    }
+
+    @Override
+    public UserProfile createProfile(Long userId) {
+        UserProfile profile = new UserProfile();
+        profile.setUserId(userId);
+        profile.setLevel("gold");
+        profile.setIsRealname(false);
+        profile.setCreatedAt(LocalDateTime.now());
+        profile.setUpdatedAt(LocalDateTime.now());
+        save(profile);
+        return profile;
+    }
+}

+ 173 - 0
src/main/java/space/anyi/serve/service/impl/WalletServiceImpl.java

@@ -0,0 +1,173 @@
+package space.anyi.serve.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import space.anyi.serve.entity.wallet.Wallet;
+import space.anyi.serve.entity.wallet.WalletTransaction;
+import space.anyi.serve.mapper.WalletMapper;
+import space.anyi.serve.mapper.WalletTransactionMapper;
+import space.anyi.serve.service.NotificationService;
+import space.anyi.serve.service.WalletService;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Service
+public class WalletServiceImpl extends ServiceImpl<WalletMapper, Wallet> implements WalletService {
+
+    private final WalletTransactionMapper walletTransactionMapper;
+    private final NotificationService notificationService;
+
+    public WalletServiceImpl(WalletTransactionMapper walletTransactionMapper,
+                             NotificationService notificationService) {
+        this.walletTransactionMapper = walletTransactionMapper;
+        this.notificationService = notificationService;
+    }
+
+    @Override
+    public Wallet getByUserId(Long userId) {
+        return getOne(new LambdaQueryWrapper<Wallet>().eq(Wallet::getUserId, userId));
+    }
+
+    @Override
+    @Transactional
+    public Wallet createWallet(Long userId) {
+        Wallet wallet = new Wallet();
+        wallet.setUserId(userId);
+        wallet.setBalance(BigDecimal.ZERO);
+        save(wallet);
+        return wallet;
+    }
+
+    @Override
+    @Transactional
+    public void deductBalance(Long userId, BigDecimal amount, Long postId, Long expertId, String requestId) {
+        Wallet wallet = getByUserId(userId);
+        if (wallet == null) throw new IllegalArgumentException("钱包不存在");
+        if (wallet.getBalance().compareTo(amount) < 0) throw new IllegalArgumentException("余额不足");
+
+        BigDecimal before = wallet.getBalance();
+        BigDecimal after = before.subtract(amount);
+        wallet.setBalance(after);
+        wallet.setUpdatedAt(LocalDateTime.now());
+        updateById(wallet);
+
+        WalletTransaction tx = new WalletTransaction();
+        tx.setUserId(userId);
+        tx.setType("tip_out");
+        tx.setAmount(amount.negate());
+        tx.setBalanceBefore(before);
+        tx.setBalanceAfter(after);
+        tx.setStatus("success");
+        tx.setCreatedAt(LocalDateTime.now());
+        tx.setUpdatedAt(LocalDateTime.now());
+        walletTransactionMapper.insert(tx);
+
+        notificationService.createNotification(userId, "tip_success", "打赏成功",
+                "您已成功打赏 ¥" + amount + ",帖子ID:" + postId);
+        notificationService.createNotification(expertId, "tip_success", "收到打赏",
+                "您收到 ¥" + amount + " 的打赏,帖子ID:" + postId);
+    }
+
+    @Override
+    @Transactional
+    public void addBalance(Long userId, BigDecimal amount, String type, String remark) {
+        Wallet wallet = getByUserId(userId);
+        if (wallet == null) throw new IllegalArgumentException("钱包不存在");
+
+        BigDecimal before = wallet.getBalance();
+        BigDecimal after = before.add(amount);
+        wallet.setBalance(after);
+        wallet.setUpdatedAt(LocalDateTime.now());
+        updateById(wallet);
+
+        WalletTransaction tx = new WalletTransaction();
+        tx.setUserId(userId);
+        tx.setType(type);
+        tx.setAmount(amount);
+        tx.setBalanceBefore(before);
+        tx.setBalanceAfter(after);
+        tx.setStatus("success");
+        tx.setRemark(remark);
+        tx.setCreatedAt(LocalDateTime.now());
+        tx.setUpdatedAt(LocalDateTime.now());
+        walletTransactionMapper.insert(tx);
+
+        if ("recharge".equals(type) || "admin_adjust".equals(type)) {
+            notificationService.createNotification(userId, "recharge_success", "充值成功",
+                    "充值 ¥" + amount + " 成功");
+        }
+    }
+
+    @Override
+    public List<WalletTransaction> getTransactions(Long userId, int page, int size) {
+        Page<WalletTransaction> p = new Page<>(page, size);
+        LambdaQueryWrapper<WalletTransaction> wrapper = new LambdaQueryWrapper<WalletTransaction>()
+                .eq(WalletTransaction::getUserId, userId)
+                .orderByDesc(WalletTransaction::getCreatedAt);
+        return walletTransactionMapper.selectPage(p, wrapper).getRecords();
+    }
+
+    @Override
+    @Transactional
+    public WalletTransaction applyWithdraw(Long userId, BigDecimal amount) {
+        Wallet wallet = getByUserId(userId);
+        if (wallet == null) throw new IllegalArgumentException("钱包不存在");
+        if (wallet.getBalance().compareTo(amount) < 0) throw new IllegalArgumentException("余额不足");
+
+        BigDecimal before = wallet.getBalance();
+        BigDecimal after = before.subtract(amount);
+        wallet.setBalance(after);
+        wallet.setUpdatedAt(LocalDateTime.now());
+        updateById(wallet);
+
+        WalletTransaction tx = new WalletTransaction();
+        tx.setUserId(userId);
+        tx.setType("withdraw");
+        tx.setAmount(amount.negate());
+        tx.setBalanceBefore(before);
+        tx.setBalanceAfter(after);
+        tx.setStatus("pending");
+        tx.setCreatedAt(LocalDateTime.now());
+        tx.setUpdatedAt(LocalDateTime.now());
+        walletTransactionMapper.insert(tx);
+
+        notificationService.createNotification(userId, "withdraw_request", "提现申请已提交",
+                "提现 ¥" + amount + " 申请已提交,等待审核");
+        return tx;
+    }
+
+    @Override
+    @Transactional
+    public void reviewWithdraw(Long transactionId, boolean approved, String rejectReason) {
+        WalletTransaction tx = walletTransactionMapper.selectById(transactionId);
+        if (tx == null) throw new IllegalArgumentException("流水不存在");
+        if (!"pending".equals(tx.getStatus())) throw new IllegalArgumentException("该流水已处理");
+
+        if (approved) {
+            tx.setStatus("success");
+            tx.setReviewTime(LocalDateTime.now());
+            walletTransactionMapper.updateById(tx);
+            notificationService.createNotification(tx.getUserId(), "withdraw_success", "提现成功",
+                    "提现 ¥" + tx.getAmount().abs() + " 已到账");
+        } else {
+            Wallet wallet = getByUserId(tx.getUserId());
+            BigDecimal before = wallet.getBalance();
+            BigDecimal after = before.add(tx.getAmount().abs());
+            wallet.setBalance(after);
+            wallet.setUpdatedAt(LocalDateTime.now());
+            updateById(wallet);
+
+            tx.setStatus("failed");
+            tx.setReviewTime(LocalDateTime.now());
+            walletTransactionMapper.updateById(tx);
+            notificationService.createNotification(tx.getUserId(), "withdraw_failed", "提现驳回",
+                    "提现 ¥" + tx.getAmount().abs() + " 被驳回" +
+                            (rejectReason != null ? ",原因:" + rejectReason : ""));
+        }
+    }
+}

+ 4 - 0
src/main/resources/mapper/AttachmentMapper.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="space.anyi.serve.mapper.AttachmentMapper">
+</mapper>

+ 4 - 0
src/main/resources/mapper/NotificationMapper.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis://DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="space.anyi.serve.mapper.NotificationMapper">
+</mapper>

+ 4 - 0
src/main/resources/mapper/OrderTipMapper.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="space.anyi.serve.mapper.OrderTipMapper">
+</mapper>

+ 4 - 0
src/main/resources/mapper/PostMapper.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="space.anyi.serve.mapper.PostMapper">
+</mapper>

+ 4 - 0
src/main/resources/mapper/PostViewRecordMapper.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="space.anyi.serve.mapper.PostViewRecordMapper">
+</mapper>

+ 4 - 0
src/main/resources/mapper/RealnameAuthMapper.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="space.anyi.serve.mapper.RealnameAuthMapper">
+</mapper>

+ 4 - 0
src/main/resources/mapper/UserProfileMapper.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="space.anyi.serve.mapper.UserProfileMapper">
+</mapper>

+ 4 - 0
src/main/resources/mapper/WalletMapper.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="space.anyi.serve.mapper.WalletMapper">
+</mapper>

+ 4 - 0
src/main/resources/mapper/WalletTransactionMapper.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="space.anyi.serve.mapper.WalletTransactionMapper">
+</mapper>