Pārlūkot izejas kodu

优化代码,添加接口

liuchengsen 2 mēneši atpakaļ
vecāks
revīzija
32baa995a7

+ 1 - 0
zhijiayun-common/src/main/java/com/xuekairui/common/ErrorCode.java

@@ -78,6 +78,7 @@ public enum ErrorCode {
     // ========== 爬虫权益 4201-4299 ==========
     CRAWLER_QUOTA_EXCEEDED(4201, "今日爬虫次数已用完"),
     CRAWLER_MONTHLY_QUOTA_EXCEEDED(4202, "本月爬虫次数已用完"),
+    CRAWLER_YEARLY_QUOTA_EXCEEDED(4205, "本年爬虫次数已用完"),
     CRAWLER_CONCURRENT_LIMIT(4203, "爬虫并发数已达上限"),
     CRAWLER_NO_PERMISSION(4204, "无爬虫使用权限"),
 

+ 7 - 99
zhijiayun-gateway/src/main/resources/db/schema.sql

@@ -19,9 +19,6 @@ DROP TABLE IF EXISTS `t_payment_order`;
 DROP TABLE IF EXISTS `t_payment_plan`;
 DROP TABLE IF EXISTS `t_watchlist`;
 DROP TABLE IF EXISTS `t_search_history`;
-DROP TABLE IF EXISTS `t_user_level_version_relation`;
-DROP TABLE IF EXISTS `t_user_level_detail`;
-DROP TABLE IF EXISTS `t_user_level_config_version`;
 DROP TABLE IF EXISTS `t_operation_audit_log`;
 DROP TABLE IF EXISTS `t_trial_quota_config`;
 DROP TABLE IF EXISTS `t_business_license`;
@@ -89,7 +86,7 @@ CREATE TABLE IF NOT EXISTS `t_user_membership` (
 CREATE TABLE IF NOT EXISTS `t_membership_benefit` (
     `id`                BIGINT        NOT NULL AUTO_INCREMENT COMMENT '主键ID',
     `level`             VARCHAR(20)   NOT NULL COMMENT '会员等级:PLUS/PRO/ULTRA',
-    `benefit_type`      VARCHAR(32)   NOT NULL COMMENT '权益类型:DAILY_QUOTA-每日加成 MONTHLY_QUOTA-每月加成 MAX_CONCURRENT-并发加成',
+    `benefit_type`      VARCHAR(32)   NOT NULL COMMENT '权益类型:DAILY_QUOTA-每日加成 MONTHLY_QUOTA-每月加成 YEARLY_QUOTA-每年加成 MAX_CONCURRENT-并发加成',
     `benefit_value`     INT           NOT NULL DEFAULT 0 COMMENT '权益数值',
     `description`       VARCHAR(256)  DEFAULT NULL COMMENT '权益说明',
     `status`            INT           NOT NULL DEFAULT 1 COMMENT '状态:0-禁用 1-启用',
@@ -243,12 +240,15 @@ CREATE TABLE IF NOT EXISTS `t_crawler_quota_grant` (
 INSERT INTO `t_membership_benefit` (`level`, `benefit_type`, `benefit_value`, `description`)
 VALUES
     ('PLUS', 'DAILY_QUOTA', 10, '普通会员:每日有效查询10次'),
-    ('PLUS', 'MONTHLY_QUOTA', 200, '普通会员:每月查询200次'),
+    ('PLUS', 'MONTHLY_QUOTA', 300, '普通会员:每月查询300次'),
+    ('PLUS', 'YEARLY_QUOTA', 3650, '普通会员:每年查询3650次(10*365)'),
     ('PRO', 'DAILY_QUOTA', 999, '高级会员:每日不限量(最高999次)'),
-    ('PRO', 'MONTHLY_QUOTA', 20000, '高级会员:每月查询20000次'),
+    ('PRO', 'MONTHLY_QUOTA', 29970, '高级会员:每月查询29970次(999*30)'),
+    ('PRO', 'YEARLY_QUOTA', 364635, '高级会员:每年查询364635次(999*365)'),
     ('PRO', 'MAX_CONCURRENT', 5, '高级会员最大并发数'),
     ('ULTRA', 'DAILY_QUOTA', 999, '超级会员:每日不限量'),
-    ('ULTRA', 'MONTHLY_QUOTA', 99999, '超级会员:每月查询99999次'),
+    ('ULTRA', 'MONTHLY_QUOTA', 29970, '超级会员:每月查询29970次(999*30)'),
+    ('ULTRA', 'YEARLY_QUOTA', 364635, '超级会员:每年查询364635次(999*365)'),
     ('ULTRA', 'MAX_CONCURRENT', 10, '超级会员最大并发数')
 ON DUPLICATE KEY UPDATE
     `benefit_value` = VALUES(`benefit_value`),
@@ -423,73 +423,6 @@ CREATE TABLE IF NOT EXISTS `t_trial_quota_config` (
     KEY `idx_effective_time` (`effective_from`, `effective_to`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='体验配额配置表';
 
--- ============================================
--- 用户等级版本管理相关表
--- ============================================
-
--- 用户等级配置版本表
-CREATE TABLE IF NOT EXISTS `t_user_level_config_version` (
-    `id`                    BIGINT        NOT NULL AUTO_INCREMENT COMMENT '主键ID',
-    `version`               VARCHAR(20)   NOT NULL COMMENT '版本号(如:v1.0, v2.0)',
-    `version_name`          VARCHAR(100)  NOT NULL COMMENT '版本名称',
-    `is_current`            TINYINT(1)    DEFAULT 0 COMMENT '是否为当前激活版本',
-    `effective_from`        DATETIME      DEFAULT NULL COMMENT '生效开始时间',
-    `effective_to`          DATETIME      DEFAULT NULL COMMENT '生效结束时间',
-    `status`                VARCHAR(20)   DEFAULT 'DRAFT' COMMENT '状态:DRAFT/ACTIVE/INACTIVE',
-    `change_log`            TEXT          DEFAULT NULL COMMENT '变更说明',
-    `created_by`            BIGINT        DEFAULT NULL COMMENT '创建人ID',
-    `activated_by`          BIGINT        DEFAULT NULL COMMENT '激活人ID',
-    `activated_at`          DATETIME      DEFAULT NULL COMMENT '激活时间',
-    `user_count`            INT           DEFAULT 0 COMMENT '关联的用户数量',
-    `create_time`           DATETIME      DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-    `update_time`           DATETIME      DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-    PRIMARY KEY (`id`),
-    UNIQUE KEY `uk_version` (`version`),
-    KEY `idx_is_current` (`is_current`),
-    KEY `idx_status` (`status`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户等级配置版本表';
-
--- 用户等级明细表
-CREATE TABLE IF NOT EXISTS `t_user_level_detail` (
-    `id`                        BIGINT        NOT NULL AUTO_INCREMENT COMMENT '主键ID',
-    `config_version_id`         BIGINT        NOT NULL COMMENT '所属配置版本ID',
-    `level_name`                VARCHAR(50)   NOT NULL COMMENT '等级名称',
-    `level_code`                VARCHAR(20)   NOT NULL COMMENT '等级编码',
-    `level_weight`              INT           NOT NULL COMMENT '等级权重',
-    `daily_quota`               INT           NOT NULL COMMENT '每日爬虫次数配额',
-    `monthly_quota`             INT           NOT NULL COMMENT '每月爬虫次数配额',
-    `max_concurrent`            INT           NOT NULL COMMENT '最大并发爬虫数',
-    `monthly_watchlist_limit`   INT           DEFAULT NULL COMMENT '每月关注品种数限制',
-    `description`               VARCHAR(500)  DEFAULT NULL COMMENT '等级描述',
-    `status`                    TINYINT(1)    DEFAULT 1 COMMENT '状态:0-禁用 1-启用',
-    `create_time`               DATETIME      DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-    `update_time`               DATETIME      DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-    PRIMARY KEY (`id`),
-    KEY `idx_config_version_id` (`config_version_id`),
-    KEY `idx_level_code` (`level_code`),
-    UNIQUE KEY `uk_version_level` (`config_version_id`, `level_code`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户等级明细表';
-
--- 用户等级版本关联表
-CREATE TABLE IF NOT EXISTS `t_user_level_version_relation` (
-    `id`                    BIGINT        NOT NULL AUTO_INCREMENT COMMENT '主键ID',
-    `user_id`               BIGINT        NOT NULL COMMENT '用户ID',
-    `config_version_id`     BIGINT        NOT NULL COMMENT '当前使用的配置版本ID',
-    `level_detail_id`       BIGINT        NOT NULL COMMENT '当前等级ID',
-    `level_code`            VARCHAR(20)   NOT NULL COMMENT '当前等级编码(冗余)',
-    `from_version_id`       BIGINT        DEFAULT NULL COMMENT '迁移来源版本ID',
-    `migrate_reason`        VARCHAR(50)   DEFAULT NULL COMMENT '迁移原因:AUTO_REGISTER/MANUAL_MIGRATE/BATCH_MIGRATE',
-    `migrated_by`           BIGINT        DEFAULT NULL COMMENT '迁移操作人ID',
-    `migrated_at`           DATETIME      DEFAULT NULL COMMENT '迁移时间',
-    `remark`                VARCHAR(500)  DEFAULT NULL COMMENT '备注说明',
-    `create_time`           DATETIME      DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-    `update_time`           DATETIME      DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-    PRIMARY KEY (`id`),
-    UNIQUE KEY `uk_user_id` (`user_id`),
-    KEY `idx_config_version_id` (`config_version_id`),
-    KEY `idx_level_code` (`level_code`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户等级版本关联表';
-
 -- ============================================
 -- 操作审计日志表
 -- ============================================
@@ -595,31 +528,6 @@ ON DUPLICATE KEY UPDATE
     `enabled`                 = VALUES(`enabled`),
     `description`             = VALUES(`description`);
 
--- 初始化MVP初始等级配置版本(v1.0)
-INSERT INTO `t_user_level_config_version` (`version`, `version_name`, `is_current`, `status`, `change_log`, `user_count`)
-VALUES
-    ('v1.0', 'MVP初始版本', 1, 'ACTIVE', '项目上线初始版本,包含3个会员等级:普通会员(PLUS)、高级会员(PRO)、超级会员(ULTRA)', 0)
-ON DUPLICATE KEY UPDATE
-    `version_name` = VALUES(`version_name`),
-    `is_current`   = VALUES(`is_current`),
-    `status`       = VALUES(`status`),
-    `change_log`   = VALUES(`change_log`);
-
--- 初始化v1.0版本的等级明细(与会员权益表保持一致)
-INSERT INTO `t_user_level_detail` (`config_version_id`, `level_name`, `level_code`, `level_weight`, `daily_quota`, `monthly_quota`, `max_concurrent`, `monthly_watchlist_limit`, `description`)
-VALUES
-    (1, '普通会员', 'PLUS', 1, 10, 200, 1, 100, '普通会员:每日有效查询10次'),
-    (1, '高级会员', 'PRO', 2, 999, 20000, 5, 500, '高级会员:每日不限量(最高999次)'),
-    (1, '超级会员', 'ULTRA', 3, 999, 99999, 10, 9999, '超级会员:连锁专用,多店铺管理')
-ON DUPLICATE KEY UPDATE
-    `level_name`              = VALUES(`level_name`),
-    `level_weight`            = VALUES(`level_weight`),
-    `daily_quota`             = VALUES(`daily_quota`),
-    `monthly_quota`           = VALUES(`monthly_quota`),
-    `max_concurrent`          = VALUES(`max_concurrent`),
-    `monthly_watchlist_limit` = VALUES(`monthly_watchlist_limit`),
-    `description`             = VALUES(`description`);
-
 -- 初始化邀请配置(奖励类型为赠送会员时长:邀请人送1个月,被邀请人送5次爬虫+2个月普通会员)
 INSERT INTO `t_invite_config` (`id`, `reward_type`, `reward_months`, `reward_crawler_count`, `max_daily_reward`, `invite_code_expire_days`, `max_invite_per_day`, `landing_title`, `landing_desc`, `app_name`, `status`)
 VALUES

+ 5 - 95
zhijiayun-gateway/src/test/resources/test-schema.sql

@@ -6,9 +6,6 @@ DROP TABLE IF EXISTS t_payment_order;
 DROP TABLE IF EXISTS t_payment_plan;
 DROP TABLE IF EXISTS t_watchlist;
 DROP TABLE IF EXISTS t_search_history;
-DROP TABLE IF EXISTS t_user_level_version_relation;
-DROP TABLE IF EXISTS t_user_level_detail;
-DROP TABLE IF EXISTS t_user_level_config_version;
 DROP TABLE IF EXISTS t_operation_audit_log;
 DROP TABLE IF EXISTS t_trial_quota_config;
 DROP TABLE IF EXISTS t_business_license;
@@ -383,108 +380,21 @@ CREATE TABLE IF NOT EXISTS t_trial_quota_config (
     update_time             TIMESTAMP     DEFAULT CURRENT_TIMESTAMP
 );
 
--- 用户等级配置版本表
-CREATE TABLE IF NOT EXISTS t_user_level_config_version (
-    id             BIGINT        AUTO_INCREMENT PRIMARY KEY,
-    version        VARCHAR(32)   NOT NULL,
-    version_name   VARCHAR(64)   DEFAULT NULL,
-    is_current     TINYINT(1)    NOT NULL DEFAULT 0,
-    effective_from TIMESTAMP     DEFAULT NULL,
-    effective_to   TIMESTAMP     DEFAULT NULL,
-    status         VARCHAR(16)   NOT NULL DEFAULT 'DRAFT',
-    change_log     TEXT          DEFAULT NULL,
-    created_by     BIGINT        DEFAULT NULL,
-    activated_by   BIGINT        DEFAULT NULL,
-    activated_at   TIMESTAMP     DEFAULT NULL,
-    user_count     INT           NOT NULL DEFAULT 0,
-    create_time    TIMESTAMP     DEFAULT CURRENT_TIMESTAMP,
-    update_time    TIMESTAMP     DEFAULT CURRENT_TIMESTAMP
-);
-
--- 用户等级明细表
-CREATE TABLE IF NOT EXISTS t_user_level_detail (
-    id                     BIGINT        AUTO_INCREMENT PRIMARY KEY,
-    config_version_id      BIGINT        NOT NULL,
-    level_name             VARCHAR(32)   NOT NULL,
-    level_code             VARCHAR(16)   NOT NULL,
-    level_weight           INT           NOT NULL DEFAULT 0,
-    daily_quota            INT           NOT NULL DEFAULT 0,
-    monthly_quota          INT           NOT NULL DEFAULT 0,
-    max_concurrent         INT           NOT NULL DEFAULT 1,
-    monthly_watchlist_limit INT          DEFAULT NULL,
-    description            VARCHAR(256)  DEFAULT NULL,
-    status                 INT           NOT NULL DEFAULT 1,
-    create_time            TIMESTAMP     DEFAULT CURRENT_TIMESTAMP,
-    update_time            TIMESTAMP     DEFAULT CURRENT_TIMESTAMP
-);
-
--- 用户等级版本关联表
-CREATE TABLE IF NOT EXISTS t_user_level_version_relation (
-    id               BIGINT        AUTO_INCREMENT PRIMARY KEY,
-    user_id          BIGINT        NOT NULL,
-    config_version_id BIGINT       NOT NULL,
-    level_detail_id  BIGINT        DEFAULT NULL,
-    level_code       VARCHAR(16)   DEFAULT NULL,
-    from_version_id  BIGINT        DEFAULT NULL,
-    migrate_reason   VARCHAR(32)   DEFAULT NULL,
-    migrated_by      BIGINT        DEFAULT NULL,
-    migrated_at      TIMESTAMP     DEFAULT NULL,
-    remark           VARCHAR(256)  DEFAULT NULL,
-    create_time      TIMESTAMP     DEFAULT CURRENT_TIMESTAMP,
-    update_time      TIMESTAMP     DEFAULT CURRENT_TIMESTAMP,
-    INDEX idx_user_id (user_id)
-);
-
 -- 初始化会员权益配置
 INSERT IGNORE INTO t_membership_benefit (level, benefit_type, benefit_value, description, status)
 VALUES
     ('PLUS', 'DAILY_QUOTA', 10, '普通会员:每日有效查询10次', 1),
     ('PLUS', 'MONTHLY_QUOTA', 300, '普通会员:每月查询300次', 1),
+    ('PLUS', 'YEARLY_QUOTA', 3650, '普通会员:每年查询3650次(10*365)', 1),
     ('PRO', 'DAILY_QUOTA', 999, '高级会员:每日不限量(最高999次)', 1),
-    ('PRO', 'MONTHLY_QUOTA', 29970, '高级会员:每月查询29970次', 1),
+    ('PRO', 'MONTHLY_QUOTA', 29970, '高级会员:每月查询29970次(999*30)', 1),
+    ('PRO', 'YEARLY_QUOTA', 364635, '高级会员:每年查询364635次(999*365)', 1),
     ('PRO', 'MAX_CONCURRENT', 5, '高级会员最大并发数', 1),
     ('ULTRA', 'DAILY_QUOTA', 999, '超级会员:每日不限量', 1),
-    ('ULTRA', 'MONTHLY_QUOTA', 29970, '超级会员:每月查询29970次', 1),
+    ('ULTRA', 'MONTHLY_QUOTA', 29970, '超级会员:每月查询29970次(999*30)', 1),
+    ('ULTRA', 'YEARLY_QUOTA', 364635, '超级会员:每年查询364635次(999*365)', 1),
     ('ULTRA', 'MAX_CONCURRENT', 10, '超级会员最大并发数', 1);
 
--- 初始化用户等级配置版本(v1.0)
-INSERT IGNORE INTO t_user_level_config_version (id, version, version_name, is_current, status, change_log, user_count)
-VALUES
-    (1, 'v1.0', 'MVP初始版本', 1, 'ACTIVE', '项目上线初始版本,包含3个会员等级', 2)
-ON DUPLICATE KEY UPDATE
-    version_name = VALUES(version_name),
-    is_current = VALUES(is_current),
-    status = VALUES(status),
-    change_log = VALUES(change_log),
-    user_count = VALUES(user_count);
-
--- 初始化v1.0版本的等级明细
-INSERT IGNORE INTO t_user_level_detail (id, config_version_id, level_name, level_code, level_weight, daily_quota, monthly_quota, max_concurrent, monthly_watchlist_limit, description)
-VALUES
-    (1, 1, '普通会员', 'PLUS', 1, 10, 300, 1, 100, '普通会员:每日有效查询10次'),
-    (2, 1, '高级会员', 'PRO', 2, 999, 29970, 5, 500, '高级会员:每日不限量(最高999次)'),
-    (3, 1, '超级会员', 'ULTRA', 3, 999, 29970, 10, 9999, '超级会员:连锁专用,多店铺管理')
-ON DUPLICATE KEY UPDATE
-    level_name = VALUES(level_name),
-    level_weight = VALUES(level_weight),
-    daily_quota = VALUES(daily_quota),
-    monthly_quota = VALUES(monthly_quota),
-    max_concurrent = VALUES(max_concurrent),
-    monthly_watchlist_limit = VALUES(monthly_watchlist_limit),
-    description = VALUES(description);
-
--- 初始化测试用户等级版本关联
-INSERT IGNORE INTO t_user_level_version_relation (user_id, config_version_id, level_detail_id, level_code, migrate_reason, migrated_at)
-VALUES
-    (1, 1, 1, 'PLUS', 'AUTO_REGISTER', CURRENT_TIMESTAMP),
-    (2, 1, 2, 'PRO', 'AUTO_REGISTER', CURRENT_TIMESTAMP)
-ON DUPLICATE KEY UPDATE
-    config_version_id = VALUES(config_version_id),
-    level_detail_id = VALUES(level_detail_id),
-    level_code = VALUES(level_code),
-    migrate_reason = VALUES(migrate_reason),
-    migrated_at = VALUES(migrated_at);
-
 -- 搜索历史表
 CREATE TABLE IF NOT EXISTS t_search_history (
     id              BIGINT AUTO_INCREMENT PRIMARY KEY,

+ 14 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/dto/CrawlerStatusResponse.java

@@ -45,6 +45,17 @@ public class CrawlerStatusResponse {
     /** 本月剩余 */
     private Integer monthRemaining;
 
+    // ========== 本年配额 ==========
+
+    /** 本年已用次数 */
+    private Integer yearUsed;
+
+    /** 本年总配额(等级基础 + 额外) */
+    private Integer yearQuota;
+
+    /** 本年剩余 */
+    private Integer yearRemaining;
+
     // ========== 配额来源明细 ==========
 
     /** 等级基础每日配额 */
@@ -53,6 +64,9 @@ public class CrawlerStatusResponse {
     /** 等级基础每月配额 */
     private Integer baseMonthlyQuota;
 
+    /** 等级基础每年配额 */
+    private Integer baseYearlyQuota;
+
     /** 额外配额总额(三种来源之和) */
     private Integer extraQuota;
 

+ 5 - 2
zhijiayun-user/src/main/java/com/xuekairui/user/dto/UserLevelResponse.java

@@ -32,16 +32,19 @@ public class UserLevelResponse {
     /** 每日爬虫配额(如:10次/天) */
     private Integer crawlerQuota;
 
-    /** 每月爬虫配额(如:200次/月) */
+    /** 每月爬虫配额(如:300次/月) */
     private Integer monthlyQuota;
 
+    /** 每年爬虫配额(如:3650次/年) */
+    private Integer yearlyQuota;
+
     /** 最大并发数 */
     private Integer maxConcurrent;
 
     /** 等级描述(如:普通会员:每日有效查询10次) */
     private String description;
 
-    /** 配置版本号(对应 t_user_level_config_version) */
+    /** 配置版本号(已废弃,统一使用会员体系) */
     private String version;
 
     // ========== 会员专属字段 ==========

+ 1 - 1
zhijiayun-user/src/main/java/com/xuekairui/user/entity/MembershipBenefit.java

@@ -28,7 +28,7 @@ public class MembershipBenefit {
     /** 会员等级:PLUS/PRO/ULTRA */
     private MembershipLevel level;
 
-    /** 权益类型:DAILY_QUOTA-每日爬虫加成 MONTHLY_QUOTA-每月爬虫加成 MAX_CONCURRENT-最大并发加成 */
+    /** 权益类型:DAILY_QUOTA-每日爬虫加成 MONTHLY_QUOTA-每月爬虫加成 YEARLY_QUOTA-每年爬虫加成 MAX_CONCURRENT-最大并发加成 */
     private String benefitType;
 
     /** 权益数值 */

+ 3 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/entity/UserLevel.java

@@ -37,6 +37,9 @@ public class UserLevel {
     /** 每月爬虫次数配额 */
     private Integer monthlyQuota;
 
+    /** 每年爬虫次数配额(如:3650次/年) */
+    private Integer yearlyQuota;
+
     /** 最大并发爬虫数 */
     private Integer maxConcurrent;
 

+ 6 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/mapper/CrawlerUsageLogMapper.java

@@ -27,6 +27,12 @@ public interface CrawlerUsageLogMapper extends BaseMapper<CrawlerUsageLog> {
     @Select("SELECT COALESCE(SUM(usage_count), 0) FROM t_crawler_usage_log WHERE user_id = #{userId} AND YEAR(usage_date) = YEAR(CURDATE()) AND MONTH(usage_date) = MONTH(CURDATE())")
     int sumMonthlyUsage(@Param("userId") Long userId);
 
+    /**
+     * 统计用户本年已使用爬虫次数
+     */
+    @Select("SELECT COALESCE(SUM(usage_count), 0) FROM t_crawler_usage_log WHERE user_id = #{userId} AND YEAR(usage_date) = YEAR(CURDATE())")
+    int sumYearlyUsage(@Param("userId") Long userId);
+
     /**
      * 统计用户各平台今日使用次数
      * @return List<Map<String, Object>> 包含 platform 和 today_usage

+ 17 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/service/CrawlerService.java

@@ -51,6 +51,7 @@ public class CrawlerService {
 
         int todayUsed = crawlerUsageLogMapper.sumTodayUsage(userId);
         int monthUsed = crawlerUsageLogMapper.sumMonthlyUsage(userId);
+        int yearUsed = crawlerUsageLogMapper.sumYearlyUsage(userId);
 
         int availableCoupons = couponService.countAvailableCoupons(userId, "CRAWLER");
 
@@ -60,13 +61,16 @@ public class CrawlerService {
         // 等级基础配额
         int baseDaily = level.getCrawlerQuota();
         int baseMonthly = level.getMonthlyQuota();
+        int baseYearly = level.getYearlyQuota() != null ? level.getYearlyQuota() : 0;
 
         // 总配额 = 等级基础 + 额外
         int totalDailyQuota = baseDaily + extraQuota;
         int totalMonthlyQuota = baseMonthly + extraQuota;
+        int totalYearlyQuota = baseYearly > 0 ? baseYearly + extraQuota : 0;
 
         int todayRemaining = Math.max(0, totalDailyQuota - todayUsed);
         int monthRemaining = Math.max(0, totalMonthlyQuota - monthUsed);
+        int yearRemaining = totalYearlyQuota > 0 ? Math.max(0, totalYearlyQuota - yearUsed) : null;
 
         // 配额来源明细
         List<QuotaBreakdownItem> breakdown = crawlerQuotaGrantService.getQuotaBreakdown(userId);
@@ -82,9 +86,14 @@ public class CrawlerService {
                 .monthUsed(monthUsed)
                 .monthQuota(totalMonthlyQuota)
                 .monthRemaining(monthRemaining)
+                // 本年
+                .yearUsed(yearUsed)
+                .yearQuota(totalYearlyQuota > 0 ? totalYearlyQuota : null)
+                .yearRemaining(yearRemaining)
                 // 来源明细
                 .baseDailyQuota(baseDaily)
                 .baseMonthlyQuota(baseMonthly)
+                .baseYearlyQuota(baseYearly > 0 ? baseYearly : null)
                 .extraQuota(extraQuota)
                 .extraQuotaBreakdown(breakdown)
                 // 其他
@@ -114,10 +123,12 @@ public class CrawlerService {
 
         int baseDaily = level.getCrawlerQuota();
         int baseMonthly = level.getMonthlyQuota();
+        int baseYearly = level.getYearlyQuota() != null ? level.getYearlyQuota() : 0;
         int extraQuota = crawlerQuotaGrantService.getAvailableExtraQuota(userId);
 
         int totalDaily = baseDaily + extraQuota;
         int totalMonthly = baseMonthly + extraQuota;
+        int totalYearly = baseYearly > 0 ? baseYearly + extraQuota : 0;
 
         if (totalDaily <= 0 && !useCouponFirst) {
             throw new BusinessException(ErrorCode.CRAWLER_NO_PERMISSION);
@@ -125,6 +136,12 @@ public class CrawlerService {
 
         int todayUsed = crawlerUsageLogMapper.sumTodayUsage(userId);
         int monthUsed = crawlerUsageLogMapper.sumMonthlyUsage(userId);
+        int yearUsed = crawlerUsageLogMapper.sumYearlyUsage(userId);
+
+        // 检查年度总限制(优先级最高)
+        if (totalYearly > 0 && yearUsed + count > totalYearly) {
+            throw new BusinessException(ErrorCode.CRAWLER_YEARLY_QUOTA_EXCEEDED);
+        }
 
         // 检查月度总限制
         if (monthUsed + count > totalMonthly) {

+ 9 - 3
zhijiayun-user/src/main/java/com/xuekairui/user/service/UserLevelService.java

@@ -46,7 +46,8 @@ public class UserLevelService {
                     .levelCode("PLUS")
                     .levelWeight(1)
                     .crawlerQuota(10)
-                    .monthlyQuota(200)
+                    .monthlyQuota(300)
+                    .yearlyQuota(3650)
                     .maxConcurrent(1)
                     .description("普通会员:每日有效查询10次")
                     .status(1)
@@ -57,7 +58,8 @@ public class UserLevelService {
                     .levelCode("PRO")
                     .levelWeight(2)
                     .crawlerQuota(999)
-                    .monthlyQuota(20000)
+                    .monthlyQuota(29970)
+                    .yearlyQuota(364635)
                     .maxConcurrent(5)
                     .description("高级会员:每日不限量(最高999次)")
                     .status(1)
@@ -68,7 +70,8 @@ public class UserLevelService {
                     .levelCode("ULTRA")
                     .levelWeight(3)
                     .crawlerQuota(999)
-                    .monthlyQuota(99999)
+                    .monthlyQuota(29970)
+                    .yearlyQuota(364635)
                     .maxConcurrent(10)
                     .description("超级会员:连锁专用,多店铺管理敬请期待")
                     .status(1)
@@ -187,6 +190,7 @@ public class UserLevelService {
                 .status(base.getStatus())
                 .crawlerQuota(base.getCrawlerQuota())
                 .monthlyQuota(base.getMonthlyQuota())
+                .yearlyQuota(base.getYearlyQuota())
                 .maxConcurrent(base.getMaxConcurrent());
 
         // 从 t_membership_benefit 读取权益并覆盖对应配额
@@ -198,6 +202,7 @@ public class UserLevelService {
             switch (benefit.getBenefitType()) {
                 case "DAILY_QUOTA" -> builder.crawlerQuota(benefit.getBenefitValue());
                 case "MONTHLY_QUOTA" -> builder.monthlyQuota(benefit.getBenefitValue());
+                case "YEARLY_QUOTA" -> builder.yearlyQuota(benefit.getBenefitValue());
                 case "MAX_CONCURRENT" -> builder.maxConcurrent(benefit.getBenefitValue());
                 default -> log.debug("未知会员权益类型: {}", benefit.getBenefitType());
             }
@@ -218,6 +223,7 @@ public class UserLevelService {
                 .levelWeight(level.getLevelWeight())
                 .crawlerQuota(level.getCrawlerQuota())
                 .monthlyQuota(level.getMonthlyQuota())
+                .yearlyQuota(level.getYearlyQuota())
                 .maxConcurrent(level.getMaxConcurrent())
                 .description(level.getDescription())
                 .version(null)