feat(product): 新增键盘商品管理模块

新增商品实体、Mapper、Service、Controller 及 VO,支持商品列表、详情、订阅等接口;同步更新 Sa-Token 放行路径与 .gitignore
This commit is contained in:
2025-12-12 14:15:30 +08:00
parent b4c35b0df3
commit 2e16183cb8
9 changed files with 403 additions and 1 deletions

1
.gitignore vendored
View File

@@ -33,3 +33,4 @@ build/
### VS Code ### ### VS Code ###
.vscode/ .vscode/
/CLAUDE.md /CLAUDE.md
/AGENTS.md

View File

@@ -93,7 +93,12 @@ public class SaTokenConfigure implements WebMvcConfigurer {
"/themes/purchase/list", "/themes/purchase/list",
"/themes/detail", "/themes/detail",
"/themes/recommended", "/themes/recommended",
"/user-themes/batch-delete" "/user-themes/batch-delete",
"/products/listByType",
"/products/detail",
"/products/inApp/list",
"/products/subscription/list"
}; };
} }
@Bean @Bean

View File

@@ -0,0 +1,71 @@
package com.yolo.keyborad.controller;
import com.yolo.keyborad.common.BaseResponse;
import com.yolo.keyborad.common.ErrorCode;
import com.yolo.keyborad.common.ResultUtils;
import com.yolo.keyborad.exception.BusinessException;
import com.yolo.keyborad.model.vo.products.KeyboardProductItemRespVO;
import com.yolo.keyborad.service.KeyboardProductItemsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/*
* @author: ziin
* @date: 2025/12/12
*/
@RestController
@Slf4j
@RequestMapping("/products")
@Tag(name = "商品", description = "商品相关接口")
public class ProductsController {
@Resource
private KeyboardProductItemsService productItemsService;
@GetMapping("/detail")
@Operation(summary = "查询商品明细", description = "根据商品ID或productId查询商品详情")
public BaseResponse<KeyboardProductItemRespVO> getProductDetail(
@RequestParam(value = "id", required = false) Long id,
@RequestParam(value = "productId", required = false) String productId
) {
if (id == null && (productId == null || productId.isBlank())) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "id 或 productId 至少传一个");
}
KeyboardProductItemRespVO result = (id != null)
? productItemsService.getProductDetailById(id)
: productItemsService.getProductDetailByProductId(productId);
return ResultUtils.success(result);
}
@GetMapping("/listByType")
@Operation(summary = "按类型查询商品列表", description = "根据商品类型查询商品列表type=all 返回全部")
public BaseResponse<List<KeyboardProductItemRespVO>> listByType(@RequestParam("type") String type) {
if (type == null || type.isBlank()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "type 不能为空");
}
List<KeyboardProductItemRespVO> result = productItemsService.listProductsByType(type);
return ResultUtils.success(result);
}
@GetMapping("/inApp/list")
@Operation(summary = "查询内购商品列表", description = "查询 type=in-app-purchase 的商品列表")
public BaseResponse<List<KeyboardProductItemRespVO>> listInAppPurchases() {
List<KeyboardProductItemRespVO> result = productItemsService.listProductsByType("in-app-purchase");
return ResultUtils.success(result);
}
@GetMapping("/subscription/list")
@Operation(summary = "查询订阅商品列表", description = "查询 type=subscription 的商品列表")
public BaseResponse<List<KeyboardProductItemRespVO>> listSubscriptions() {
List<KeyboardProductItemRespVO> result = productItemsService.listProductsByType("subscription");
return ResultUtils.success(result);
}
}

View File

@@ -0,0 +1,12 @@
package com.yolo.keyborad.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yolo.keyborad.model.entity.KeyboardProductItems;
/*
* @author: ziin
* @date: 2025/12/12 13:44
*/
public interface KeyboardProductItemsMapper extends BaseMapper<KeyboardProductItems> {
}

View File

@@ -0,0 +1,111 @@
package com.yolo.keyborad.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
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.util.Date;
import lombok.Data;
/*
* @author: ziin
* @date: 2025/12/12 13:44
*/
@Schema
@Data
@TableName(value = "keyboard_product_items")
public class KeyboardProductItems {
/**
* 主键,自增,唯一标识每个产品项
*/
@TableId(value = "id", type = IdType.AUTO)
@Schema(description="主键,自增,唯一标识每个产品项")
private Long id;
/**
* 产品标识符,唯一标识每个产品(如 com.loveKey.nyx.2month
*/
@TableField(value = "product_id")
@Schema(description="产品标识符,唯一标识每个产品(如 com.loveKey.nyx.2month")
private String productId;
/**
* 产品类型区分订阅subscription和内购in-app-purchase
*/
@TableField(value = "\"type\"")
@Schema(description="产品类型区分订阅subscription和内购in-app-purchase")
private String type;
/**
* 产品名称(如 100, 2
*/
@TableField(value = "\"name\"")
@Schema(description="产品名称(如 100, 2")
private String name;
/**
* 产品单位(如 金币,个月)
*/
@TableField(value = "unit")
@Schema(description="产品单位(如 金币,个月)")
private String unit;
/**
* 订阅时长的数值部分(如 2
*/
@TableField(value = "duration_value")
@Schema(description="订阅时长的数值部分(如 2")
private Integer durationValue;
/**
* 订阅时长的单位部分(如 月,天)
*/
@TableField(value = "duration_unit")
@Schema(description="订阅时长的单位部分(如 月,天)")
private String durationUnit;
/**
* 产品价格
*/
@TableField(value = "price")
@Schema(description="产品价格")
private BigDecimal price;
/**
* 产品的货币单位,如美元($
*/
@TableField(value = "currency")
@Schema(description="产品的货币单位,如美元($")
private String currency;
/**
* 产品的描述,提供更多细节信息
*/
@TableField(value = "description")
@Schema(description="产品的描述,提供更多细节信息")
private String description;
/**
* 产品项的创建时间,默认当前时间
*/
@TableField(value = "created_at")
@Schema(description="产品项的创建时间,默认当前时间")
private Date createdAt;
/**
* 产品项的最后更新时间,更新时自动设置为当前时间
*/
@TableField(value = "updated_at")
@Schema(description="产品项的最后更新时间,更新时自动设置为当前时间")
private Date updatedAt;
/**
* 订阅时长的具体天数
*/
@TableField(value = "duration_days")
@Schema(description="订阅时长的具体天数")
private Integer durationDays;
}

View File

@@ -0,0 +1,48 @@
package com.yolo.keyborad.model.vo.products;
import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;
import lombok.Data;
/**
* 商品明细返回 VO
*/
@Data
@Schema(description = "商品明细返回对象")
public class KeyboardProductItemRespVO {
@Schema(description = "主键ID")
private Long id;
@Schema(description = "产品标识符,如 com.loveKey.nyx.2month")
private String productId;
@Schema(description = "产品类型subscription / in-app-purchase")
private String type;
@Schema(description = "产品名称")
private String name;
@Schema(description = "产品单位")
private String unit;
@Schema(description = "订阅时长数值")
private Integer durationValue;
@Schema(description = "订阅时长单位")
private String durationUnit;
@Schema(description = "订阅时长天数")
private Integer durationDays;
@Schema(description = "价格")
private BigDecimal price;
@Schema(description = "货币单位")
private String currency;
@Schema(description = "描述")
private String description;
}

View File

@@ -0,0 +1,39 @@
package com.yolo.keyborad.service;
import com.yolo.keyborad.model.entity.KeyboardProductItems;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yolo.keyborad.model.vo.products.KeyboardProductItemRespVO;
import java.util.List;
/*
* @author: ziin
* @date: 2025/12/12 13:44
*/
public interface KeyboardProductItemsService extends IService<KeyboardProductItems>{
/**
* 根据主键ID查询商品明细
*
* @param id 商品主键ID
* @return 商品明细(不存在返回 null
*/
KeyboardProductItemRespVO getProductDetailById(Long id);
/**
* 根据 Apple productId 查询商品明细
*
* @param productId 商品 productId
* @return 商品明细(不存在返回 null
*/
KeyboardProductItemRespVO getProductDetailByProductId(String productId);
/**
* 根据商品类型查询商品列表
* type=all 时返回全部
*
* @param type 商品类型subscription / in-app-purchase / all
* @return 商品列表
*/
List<KeyboardProductItemRespVO> listProductsByType(String type);
}

View File

@@ -0,0 +1,89 @@
package com.yolo.keyborad.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.yolo.keyborad.model.vo.products.KeyboardProductItemRespVO;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yolo.keyborad.model.entity.KeyboardProductItems;
import com.yolo.keyborad.mapper.KeyboardProductItemsMapper;
import com.yolo.keyborad.service.KeyboardProductItemsService;
/*
* @author: ziin
* @date: 2025/12/12 13:44
*/
@Service
public class KeyboardProductItemsServiceImpl extends ServiceImpl<KeyboardProductItemsMapper, KeyboardProductItems> implements KeyboardProductItemsService{
/**
* 根据ID获取产品详情
*
* @param id 产品ID
* @return 产品详情响应对象如果ID为空或未找到产品则返回null
*/
@Override
public KeyboardProductItemRespVO getProductDetailById(Long id) {
// 参数校验ID不能为空
if (id == null) {
return null;
}
// 根据ID查询产品信息
KeyboardProductItems item = this.getById(id);
// 将实体对象转换为响应VO对象并返回
return item == null ? null : BeanUtil.copyProperties(item, KeyboardProductItemRespVO.class);
}
/**
* 根据产品ID获取产品详情
*
* @param productId 产品ID
* @return 产品详情响应对象如果产品ID为空或未找到产品则返回null
*/
@Override
public KeyboardProductItemRespVO getProductDetailByProductId(String productId) {
// 参数校验产品ID不能为空
if (productId == null || productId.isBlank()) {
return null;
}
// 根据产品ID查询产品信息
KeyboardProductItems item = this.lambdaQuery()
.eq(KeyboardProductItems::getProductId, productId)
.one();
// 将实体对象转换为响应VO对象并返回
return item == null ? null : BeanUtil.copyProperties(item, KeyboardProductItemRespVO.class);
}
/**
* 根据类型获取产品列表
*
* @param type 产品类型如果为null、空字符串或"all"则查询所有产品
* @return 产品详情响应列表按ID升序排列
*/
@Override
public java.util.List<KeyboardProductItemRespVO> listProductsByType(String type) {
// 创建Lambda查询构造器
var query = this.lambdaQuery();
// 如果类型参数有效且不是"all",则添加类型过滤条件
if (type != null && !type.isBlank() && !"all".equalsIgnoreCase(type)) {
query.eq(KeyboardProductItems::getType, type);
}
// 执行查询按ID升序排列
java.util.List<KeyboardProductItems> items = query
.orderByAsc(KeyboardProductItems::getId)
.list();
// 将实体对象转换为响应VO对象并返回
return items.stream()
.map(i -> BeanUtil.copyProperties(i, KeyboardProductItemRespVO.class))
.toList();
}
}

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yolo.keyborad.mapper.KeyboardProductItemsMapper">
<resultMap id="BaseResultMap" type="com.yolo.keyborad.model.entity.KeyboardProductItems">
<!--@mbg.generated-->
<!--@Table keyboard_product_items-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="product_id" jdbcType="VARCHAR" property="productId" />
<result column="type" jdbcType="VARCHAR" property="type" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="unit" jdbcType="VARCHAR" property="unit" />
<result column="duration_value" jdbcType="INTEGER" property="durationValue" />
<result column="duration_unit" jdbcType="VARCHAR" property="durationUnit" />
<result column="price" jdbcType="NUMERIC" property="price" />
<result column="currency" jdbcType="VARCHAR" property="currency" />
<result column="description" jdbcType="VARCHAR" property="description" />
<result column="created_at" jdbcType="TIMESTAMP" property="createdAt" />
<result column="updated_at" jdbcType="TIMESTAMP" property="updatedAt" />
<result column="duration_days" jdbcType="INTEGER" property="durationDays" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, product_id, "type", "name", unit, duration_value, duration_unit, price, currency,
description, created_at, updated_at, duration_days
</sql>
</mapper>