Browse Source

feat(generator): 优化模板生成逻辑并添加新功能

- 改进 index.tsx 模板,增加智能字段检测和条件性功能生成
- 修复字段注释为空时模板渲染失败的问题
- 添加自动更新 app.config.ts 页面路径的功能
- 新增 ShopArticle相关的实体、Mapper、Service 等代码
- 优化 add.tsx 和 add.config.ts模板,提高用户体验
main
科技小王子 2 months ago
parent
commit
eac1102eb1
  1. 108
      docs/INDEX_TSX_IMPROVEMENTS.md
  2. 8
      docs/MOBILE_GENERATOR_SUMMARY.md
  3. 91
      docs/TEMPLATE_FIXES.md
  4. 82
      docs/update_app_config.sh
  5. 120
      src/main/java/com/gxwebsoft/shop/controller/ShopArticleController.java
  6. 192
      src/main/java/com/gxwebsoft/shop/entity/ShopArticle.java
  7. 37
      src/main/java/com/gxwebsoft/shop/mapper/ShopArticleMapper.java
  8. 192
      src/main/java/com/gxwebsoft/shop/mapper/xml/ShopArticleMapper.xml
  9. 207
      src/main/java/com/gxwebsoft/shop/param/ShopArticleParam.java
  10. 42
      src/main/java/com/gxwebsoft/shop/service/ShopArticleService.java
  11. 47
      src/main/java/com/gxwebsoft/shop/service/impl/ShopArticleServiceImpl.java
  12. 88
      src/test/java/com/gxwebsoft/generator/CmsGenerator.java
  13. 87
      src/test/java/com/gxwebsoft/generator/ShopGenerator.java
  14. 2
      src/test/java/com/gxwebsoft/generator/templates/add.config.ts.btl
  15. 12
      src/test/java/com/gxwebsoft/generator/templates/add.tsx.btl
  16. 8
      src/test/java/com/gxwebsoft/generator/templates/controller.java.btl
  17. 2
      src/test/java/com/gxwebsoft/generator/templates/index.config.ts.btl
  18. 52
      src/test/java/com/gxwebsoft/generator/templates/index.tsx.btl

108
docs/INDEX_TSX_IMPROVEMENTS.md

@ -0,0 +1,108 @@
# index.tsx 模板改进说明
## 🔍 发现的问题
### 1. 硬编码字段名
**问题**:原模板假设所有表都有 `name``description` 字段
```typescript
// 原来的硬编码方式
<View>{item.name}</View>
<View>{item.description}</View>
```
### 2. 固定的业务逻辑
**问题**:所有表都生成"默认选项"功能,即使表中没有 `isDefault` 字段
### 3. 不够灵活的显示方式
**问题**:没有根据实际表结构动态调整显示内容
## ✅ 改进内容
### 1. 智能字段检测
```typescript
<% var hasIsDefaultField = false; %>
<% for(field in table.fields){ %>
<% if(field.propertyName == 'isDefault'){ %>
<% hasIsDefaultField = true; %>
<% } %>
<% } %>
```
### 2. 条件性功能生成
- **有 `isDefault` 字段**:生成完整的默认选项功能
- **无 `isDefault` 字段**:只生成基本的列表和编辑功能
### 3. 动态字段显示
```typescript
<% var displayFields = []; %>
<% for(field in table.fields){ %>
<% if(field.propertyName != 'id' && field.propertyName != 'createTime' && field.propertyName != 'updateTime' && field.propertyName != 'isDefault'){ %>
<% displayFields.add(field); %>
<% } %>
<% } %>
```
自动选择前两个可显示字段作为主要显示内容。
## 🎯 改进效果
### 有 `isDefault` 字段的表(如地址、支付方式)
- ✅ 生成默认选项设置功能
- ✅ 支持点击选择默认项
- ✅ 显示默认选项状态图标
### 无 `isDefault` 字段的表(如商品、分类)
- ✅ 只生成基本的列表显示
- ✅ 支持编辑和删除操作
- ✅ 不生成不必要的默认选项功能
### 字段显示逻辑
- **第一个字段**:作为主标题显示(较大字体)
- **第二个字段**:作为副标题显示(较小字体)
- **自动过滤**:排除 `id`、`createTime`、`updateTime`、`isDefault` 等系统字段
## 📋 生成示例
### 地址表(有 isDefault 字段)
```typescript
// 会生成完整的默认地址功能
const selectItem = async (item: ShopUserAddress) => {
// 设置默认地址逻辑
}
// 显示默认选项图标
{item.isDefault ? <Checked /> : <CheckNormal />}
```
### 商品表(无 isDefault 字段)
```typescript
// 只生成基本列表功能,无默认选项相关代码
<Cell className={'flex flex-col gap-1'}>
<View>{item.name}</View> // 第一个字段
<View>{item.description}</View> // 第二个字段
</Cell>
```
## 🔧 技术实现
### Beetl 模板语法
- `<% var hasIsDefaultField = false; %>` - 定义变量
- `<% displayFields.add(field); %>` - 数组操作
- `<% if(hasIsDefaultField){ %>` - 条件判断
- `${displayFields[0].propertyName}` - 动态字段访问
### 字段过滤规则
排除以下系统字段:
- `id` - 主键
- `createTime` - 创建时间
- `updateTime` - 更新时间
- `isDefault` - 默认标志(单独处理)
## 🎉 优势
1. **更加通用**:适用于各种不同结构的表
2. **智能适配**:根据表结构自动调整功能
3. **减少冗余**:不生成不必要的代码
4. **更好维护**:生成的代码更符合实际业务需求
现在生成的移动端列表页面更加智能和实用了!

8
docs/MOBILE_GENERATOR_SUMMARY.md

@ -62,10 +62,16 @@ private static final String[] TABLE_NAMES = new String[]{
# 运行商城模块生成器
java com.gxwebsoft.generator.ShopGenerator
# 运行CMS模块生成器
# 运行CMS模块生成器
java com.gxwebsoft.generator.CmsGenerator
```
**🎉 新功能:自动更新 app.config.ts**
- 生成器现在会自动更新 `app.config.ts` 文件
- 自动添加新生成页面的路径配置
- 自动备份原文件,避免数据丢失
- 避免重复添加已存在的页面路径
### 3. 检查生成结果
生成的文件位于:
```

91
docs/TEMPLATE_FIXES.md

@ -0,0 +1,91 @@
# 模板修复说明
## 🔧 修复的问题
### 1. 字段注释为空的问题
**问题描述**:
- 当数据库表的字段没有注释时,模板渲染会失败
- 错误信息:`field.comment为空`
**修复内容**:
#### add.tsx.btl 模板
- **修复前**:`${field.comment!}` - 注释为空时显示空字符串
- **修复后**:`${field.comment!'字段'}` - 注释为空时显示默认值
具体修改:
```typescript
// 标签显示
label="${field.comment!field.propertyName}"
// 输入框提示
placeholder="请输入${field.comment!'字段'}"
placeholder="请输入${field.comment!'内容'}"
// 条件判断
<% if(field.propertyType == 'String' && field.comment?? && (field.comment?contains('描述') || field.comment?contains('备注') || field.comment?contains('内容'))){ %>
```
#### 配置文件模板
- **index.config.ts.btl**:`'${table.comment!'数据'}管理'`
- **add.config.ts.btl**:`'新增${table.comment!'数据'}'`
### 2. 智能 userId 字段检测
**问题描述**:
- 所有表都生成设置 userId 的代码,即使表中没有 user_id 字段
**修复内容**:
#### controller.java.btl 模板
添加了字段检测逻辑:
```java
<% var hasUserIdField = false; %>
<% for(field in table.fields){ %>
<% if(field.propertyName == 'userId'){ %>
<% hasUserIdField = true; %>
<% } %>
<% } %>
<% if(hasUserIdField){ %>
// 记录当前登录用户id
User loginUser = getLoginUser();
if (loginUser != null) {
${table.entityPath}.setUserId(loginUser.getUserId());
}
<% } %>
```
## ✅ 修复效果
### 1. 空注释处理
- **有注释的字段**:正常显示字段注释
- **无注释的字段**:显示字段名或默认提示文本
- **空表注释**:显示"数据"作为默认值
### 2. 智能 userId 处理
- **有 user_id 字段的表**:生成完整的用户ID设置代码
- **无 user_id 字段的表**:不生成用户ID相关代码
## 🎯 Beetl 模板语法说明
### 空值处理
- `${field.comment!}` - 为空时显示空字符串
- `${field.comment!'默认值'}` - 为空时显示默认值
- `${field.comment!field.propertyName}` - 为空时显示字段名
### 条件判断
- `field.comment??` - 检查字段是否不为null
- `field.comment?contains('文本')` - 检查字段是否包含指定文本
### 变量定义
- `<% var hasUserIdField = false; %>` - 定义布尔变量
- `<% if(hasUserIdField){ %>` - 条件判断
## 🚀 使用建议
1. **数据库设计**:建议为表和字段添加有意义的注释
2. **模板测试**:生成代码前先测试模板在各种数据情况下的表现
3. **错误处理**:模板中添加适当的默认值和空值处理
现在模板更加健壮,能够处理各种边界情况!

82
docs/update_app_config.sh

@ -0,0 +1,82 @@
#!/bin/bash
# 自动更新 app.config.ts 页面路径的脚本
APP_CONFIG_PATH="/Users/gxwebsoft/VUE/template-10550/src/app.config.ts"
SRC_PATH="/Users/gxwebsoft/VUE/template-10550/src"
echo "=== 自动更新 app.config.ts 页面路径 ==="
echo ""
# 检查 app.config.ts 是否存在
if [ ! -f "$APP_CONFIG_PATH" ]; then
echo "❌ app.config.ts 文件不存在: $APP_CONFIG_PATH"
exit 1
fi
echo "✅ 找到 app.config.ts 文件"
# 备份原文件
cp "$APP_CONFIG_PATH" "$APP_CONFIG_PATH.backup.$(date +%Y%m%d_%H%M%S)"
echo "✅ 已备份原文件"
# 查找所有生成的页面路径配置文件
echo ""
echo "🔍 查找生成的页面路径配置:"
# 查找 shop 模块的页面
SHOP_PAGES=""
if [ -d "$SRC_PATH/shop" ]; then
for dir in "$SRC_PATH/shop"/*; do
if [ -d "$dir" ] && [ -f "$dir/index.tsx" ] && [ -f "$dir/add.tsx" ]; then
page_name=$(basename "$dir")
echo " 找到 shop 页面: $page_name"
SHOP_PAGES="$SHOP_PAGES '$page_name/index',\n '$page_name/add',\n"
fi
done
fi
# 查找 cms 模块的页面
CMS_PAGES=""
if [ -d "$SRC_PATH/cms" ]; then
for dir in "$SRC_PATH/cms"/*; do
if [ -d "$dir" ] && [ -f "$dir/index.tsx" ] && [ -f "$dir/add.tsx" ]; then
page_name=$(basename "$dir")
echo " 找到 cms 页面: $page_name"
CMS_PAGES="$CMS_PAGES '$page_name/index',\n '$page_name/add',\n"
fi
done
fi
echo ""
echo "📝 需要添加到 app.config.ts 的页面路径:"
echo ""
if [ -n "$SHOP_PAGES" ]; then
echo "Shop 模块页面:"
echo -e "$SHOP_PAGES"
fi
if [ -n "$CMS_PAGES" ]; then
echo "CMS 模块页面:"
echo -e "$CMS_PAGES"
fi
echo ""
echo "⚠️ 请手动将上述页面路径添加到 app.config.ts 的对应子包中"
echo ""
echo "示例:"
echo "在 shop 子包的 pages 数组中添加:"
if [ -n "$SHOP_PAGES" ]; then
echo -e "$SHOP_PAGES"
fi
echo ""
echo "在 cms 子包的 pages 数组中添加:"
if [ -n "$CMS_PAGES" ]; then
echo -e "$CMS_PAGES"
fi
echo ""
echo "=== 完成 ==="
echo "备份文件位置: $APP_CONFIG_PATH.backup.*"

120
src/main/java/com/gxwebsoft/shop/controller/ShopArticleController.java

@ -0,0 +1,120 @@
package com.gxwebsoft.shop.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.shop.service.ShopArticleService;
import com.gxwebsoft.shop.entity.ShopArticle;
import com.gxwebsoft.shop.param.ShopArticleParam;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.system.entity.User;
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 javax.annotation.Resource;
import java.util.List;
/**
* 商品文章控制器
*
* @author 科技小王子
* @since 2025-08-13 00:28:23
*/
@Tag(name = "商品文章管理")
@RestController
@RequestMapping("/api/shop/shop-article")
public class ShopArticleController extends BaseController {
@Resource
private ShopArticleService shopArticleService;
@Operation(summary = "分页查询商品文章")
@GetMapping("/page")
public ApiResult<PageResult<ShopArticle>> page(ShopArticleParam param) {
// 使用关联查询
return success(shopArticleService.pageRel(param));
}
@Operation(summary = "查询全部商品文章")
@GetMapping()
public ApiResult<List<ShopArticle>> list(ShopArticleParam param) {
// 使用关联查询
return success(shopArticleService.listRel(param));
}
@Operation(summary = "根据id查询商品文章")
@GetMapping("/{id}")
public ApiResult<ShopArticle> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(shopArticleService.getByIdRel(id));
}
@OperationLog
@Operation(summary = "添加商品文章")
@PostMapping()
public ApiResult<?> save(@RequestBody ShopArticle shopArticle) {
// 记录当前登录用户id
User loginUser = getLoginUser();
if (loginUser != null) {
shopArticle.setUserId(loginUser.getUserId());
}
if (shopArticleService.save(shopArticle)) {
return success("添加成功");
}
return fail("添加失败");
}
@OperationLog
@Operation(summary = "修改商品文章")
@PutMapping()
public ApiResult<?> update(@RequestBody ShopArticle shopArticle) {
if (shopArticleService.updateById(shopArticle)) {
return success("修改成功");
}
return fail("修改失败");
}
@OperationLog
@Operation(summary = "删除商品文章")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (shopArticleService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@OperationLog
@Operation(summary = "批量添加商品文章")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<ShopArticle> list) {
if (shopArticleService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@OperationLog
@Operation(summary = "批量修改商品文章")
@PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<ShopArticle> batchParam) {
if (batchParam.update(shopArticleService, "article_id")) {
return success("修改成功");
}
return fail("修改失败");
}
@OperationLog
@Operation(summary = "批量删除商品文章")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (shopArticleService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

192
src/main/java/com/gxwebsoft/shop/entity/ShopArticle.java

@ -0,0 +1,192 @@
package com.gxwebsoft.shop.entity;
import java.math.BigDecimal;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableLogic;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 商品文章
*
* @author 科技小王子
* @since 2025-08-13 00:28:23
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "ShopArticle对象", description = "商品文章")
public class ShopArticle implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "文章ID")
@TableId(value = "article_id", type = IdType.AUTO)
private Integer articleId;
@Schema(description = "文章标题")
private String title;
@Schema(description = "文章类型 0常规 1视频")
private Integer type;
@Schema(description = "模型")
private String model;
@Schema(description = "详情页模板")
private String detail;
@Schema(description = "文章分类ID")
private Integer categoryId;
@Schema(description = "上级id, 0是顶级")
private Integer parentId;
@Schema(description = "话题")
private String topic;
@Schema(description = "标签")
private String tags;
@Schema(description = "封面图")
private String image;
@Schema(description = "封面图宽")
private Integer imageWidth;
@Schema(description = "封面图高")
private Integer imageHeight;
@Schema(description = "付费金额")
private BigDecimal price;
@Schema(description = "开始时间")
private LocalDateTime startTime;
@Schema(description = "结束时间")
private LocalDateTime endTime;
@Schema(description = "来源")
private String source;
@Schema(description = "产品概述")
private String overview;
@Schema(description = "虚拟阅读量(仅用作展示)")
private Integer virtualViews;
@Schema(description = "实际阅读量")
private Integer actualViews;
@Schema(description = "评分")
private BigDecimal rate;
@Schema(description = "列表显示方式(10小图展示 20大图展示)")
private Integer showType;
@Schema(description = "访问密码")
private String password;
@Schema(description = "可见类型 0所有人 1登录可见 2密码可见")
private Integer permission;
@Schema(description = "发布来源客户端 (APP、H5、小程序等)")
private String platform;
@Schema(description = "文章附件")
private String files;
@Schema(description = "视频地址")
private String video;
@Schema(description = "接受的文件类型")
private String accept;
@Schema(description = "经度")
private String longitude;
@Schema(description = "纬度")
private String latitude;
@Schema(description = "所在省份")
private String province;
@Schema(description = "所在城市")
private String city;
@Schema(description = "所在辖区")
private String region;
@Schema(description = "街道地址")
private String address;
@Schema(description = "点赞数")
private Integer likes;
@Schema(description = "评论数")
private Integer commentNumbers;
@Schema(description = "提醒谁看")
private String toUsers;
@Schema(description = "作者")
private String author;
@Schema(description = "推荐")
private Integer recommend;
@Schema(description = "报名人数")
private Integer bmUsers;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "商户ID")
private Integer merchantId;
@Schema(description = "项目ID")
private Integer projectId;
@Schema(description = "语言")
private String lang;
@Schema(description = "关联默认语言的文章ID")
private Integer langArticleId;
@Schema(description = "是否自动翻译")
private Boolean translation;
@Schema(description = "编辑器类型 0 Markdown编辑器 1 富文本编辑器 ")
private Boolean editor;
@Schema(description = "pdf文件地址")
private String pdfUrl;
@Schema(description = "版本号")
private Integer version;
@Schema(description = "排序(数字越小越靠前)")
private Integer sortNumber;
@Schema(description = "备注")
private String comments;
@Schema(description = "状态, 0已发布, 1待审核 2已驳回 3违规内容")
private Integer status;
@Schema(description = "是否删除, 0否, 1是")
@TableLogic
private Integer deleted;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "修改时间")
private LocalDateTime updateTime;
}

37
src/main/java/com/gxwebsoft/shop/mapper/ShopArticleMapper.java

@ -0,0 +1,37 @@
package com.gxwebsoft.shop.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.shop.entity.ShopArticle;
import com.gxwebsoft.shop.param.ShopArticleParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 商品文章Mapper
*
* @author 科技小王子
* @since 2025-08-13 00:28:23
*/
public interface ShopArticleMapper extends BaseMapper<ShopArticle> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<ShopArticle>
*/
List<ShopArticle> selectPageRel(@Param("page") IPage<ShopArticle> page,
@Param("param") ShopArticleParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<ShopArticle> selectListRel(@Param("param") ShopArticleParam param);
}

192
src/main/java/com/gxwebsoft/shop/mapper/xml/ShopArticleMapper.xml

@ -0,0 +1,192 @@
<?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.gxwebsoft.shop.mapper.ShopArticleMapper">
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*
FROM shop_article a
<where>
<if test="param.articleId != null">
AND a.article_id = #{param.articleId}
</if>
<if test="param.title != null">
AND a.title LIKE CONCAT('%', #{param.title}, '%')
</if>
<if test="param.type != null">
AND a.type = #{param.type}
</if>
<if test="param.model != null">
AND a.model LIKE CONCAT('%', #{param.model}, '%')
</if>
<if test="param.detail != null">
AND a.detail LIKE CONCAT('%', #{param.detail}, '%')
</if>
<if test="param.categoryId != null">
AND a.category_id = #{param.categoryId}
</if>
<if test="param.parentId != null">
AND a.parent_id = #{param.parentId}
</if>
<if test="param.topic != null">
AND a.topic LIKE CONCAT('%', #{param.topic}, '%')
</if>
<if test="param.tags != null">
AND a.tags LIKE CONCAT('%', #{param.tags}, '%')
</if>
<if test="param.image != null">
AND a.image LIKE CONCAT('%', #{param.image}, '%')
</if>
<if test="param.imageWidth != null">
AND a.image_width = #{param.imageWidth}
</if>
<if test="param.imageHeight != null">
AND a.image_height = #{param.imageHeight}
</if>
<if test="param.price != null">
AND a.price = #{param.price}
</if>
<if test="param.startTime != null">
AND a.start_time LIKE CONCAT('%', #{param.startTime}, '%')
</if>
<if test="param.endTime != null">
AND a.end_time LIKE CONCAT('%', #{param.endTime}, '%')
</if>
<if test="param.source != null">
AND a.source LIKE CONCAT('%', #{param.source}, '%')
</if>
<if test="param.overview != null">
AND a.overview LIKE CONCAT('%', #{param.overview}, '%')
</if>
<if test="param.virtualViews != null">
AND a.virtual_views = #{param.virtualViews}
</if>
<if test="param.actualViews != null">
AND a.actual_views = #{param.actualViews}
</if>
<if test="param.rate != null">
AND a.rate = #{param.rate}
</if>
<if test="param.showType != null">
AND a.show_type = #{param.showType}
</if>
<if test="param.password != null">
AND a.password LIKE CONCAT('%', #{param.password}, '%')
</if>
<if test="param.permission != null">
AND a.permission = #{param.permission}
</if>
<if test="param.platform != null">
AND a.platform LIKE CONCAT('%', #{param.platform}, '%')
</if>
<if test="param.files != null">
AND a.files LIKE CONCAT('%', #{param.files}, '%')
</if>
<if test="param.video != null">
AND a.video LIKE CONCAT('%', #{param.video}, '%')
</if>
<if test="param.accept != null">
AND a.accept LIKE CONCAT('%', #{param.accept}, '%')
</if>
<if test="param.longitude != null">
AND a.longitude LIKE CONCAT('%', #{param.longitude}, '%')
</if>
<if test="param.latitude != null">
AND a.latitude LIKE CONCAT('%', #{param.latitude}, '%')
</if>
<if test="param.province != null">
AND a.province LIKE CONCAT('%', #{param.province}, '%')
</if>
<if test="param.city != null">
AND a.city LIKE CONCAT('%', #{param.city}, '%')
</if>
<if test="param.region != null">
AND a.region LIKE CONCAT('%', #{param.region}, '%')
</if>
<if test="param.address != null">
AND a.address LIKE CONCAT('%', #{param.address}, '%')
</if>
<if test="param.likes != null">
AND a.likes = #{param.likes}
</if>
<if test="param.commentNumbers != null">
AND a.comment_numbers = #{param.commentNumbers}
</if>
<if test="param.toUsers != null">
AND a.to_users LIKE CONCAT('%', #{param.toUsers}, '%')
</if>
<if test="param.author != null">
AND a.author LIKE CONCAT('%', #{param.author}, '%')
</if>
<if test="param.recommend != null">
AND a.recommend = #{param.recommend}
</if>
<if test="param.bmUsers != null">
AND a.bm_users = #{param.bmUsers}
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
<if test="param.merchantId != null">
AND a.merchant_id = #{param.merchantId}
</if>
<if test="param.projectId != null">
AND a.project_id = #{param.projectId}
</if>
<if test="param.lang != null">
AND a.lang LIKE CONCAT('%', #{param.lang}, '%')
</if>
<if test="param.langArticleId != null">
AND a.lang_article_id = #{param.langArticleId}
</if>
<if test="param.translation != null">
AND a.translation = #{param.translation}
</if>
<if test="param.editor != null">
AND a.editor = #{param.editor}
</if>
<if test="param.pdfUrl != null">
AND a.pdf_url LIKE CONCAT('%', #{param.pdfUrl}, '%')
</if>
<if test="param.version != null">
AND a.version = #{param.version}
</if>
<if test="param.sortNumber != null">
AND a.sort_number = #{param.sortNumber}
</if>
<if test="param.comments != null">
AND a.comments LIKE CONCAT('%', #{param.comments}, '%')
</if>
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.deleted != null">
AND a.deleted = #{param.deleted}
</if>
<if test="param.deleted == null">
AND a.deleted = 0
</if>
<if test="param.createTimeStart != null">
AND a.create_time &gt;= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null">
AND a.create_time &lt;= #{param.createTimeEnd}
</if>
<if test="param.keywords != null">
AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%')
)
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.shop.entity.ShopArticle">
<include refid="selectSql"></include>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.shop.entity.ShopArticle">
<include refid="selectSql"></include>
</select>
</mapper>

207
src/main/java/com/gxwebsoft/shop/param/ShopArticleParam.java

@ -0,0 +1,207 @@
package com.gxwebsoft.shop.param;
import java.math.BigDecimal;
import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType;
import com.gxwebsoft.common.core.web.BaseParam;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 商品文章查询参数
*
* @author 科技小王子
* @since 2025-08-13 00:28:23
*/
@Data
@EqualsAndHashCode(callSuper = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "ShopArticleParam对象", description = "商品文章查询参数")
public class ShopArticleParam extends BaseParam {
private static final long serialVersionUID = 1L;
@Schema(description = "文章ID")
@QueryField(type = QueryType.EQ)
private Integer articleId;
@Schema(description = "文章标题")
private String title;
@Schema(description = "文章类型 0常规 1视频")
@QueryField(type = QueryType.EQ)
private Integer type;
@Schema(description = "模型")
private String model;
@Schema(description = "详情页模板")
private String detail;
@Schema(description = "文章分类ID")
@QueryField(type = QueryType.EQ)
private Integer categoryId;
@Schema(description = "上级id, 0是顶级")
@QueryField(type = QueryType.EQ)
private Integer parentId;
@Schema(description = "话题")
private String topic;
@Schema(description = "标签")
private String tags;
@Schema(description = "封面图")
private String image;
@Schema(description = "封面图宽")
@QueryField(type = QueryType.EQ)
private Integer imageWidth;
@Schema(description = "封面图高")
@QueryField(type = QueryType.EQ)
private Integer imageHeight;
@Schema(description = "付费金额")
@QueryField(type = QueryType.EQ)
private BigDecimal price;
@Schema(description = "开始时间")
private String startTime;
@Schema(description = "结束时间")
private String endTime;
@Schema(description = "来源")
private String source;
@Schema(description = "产品概述")
private String overview;
@Schema(description = "虚拟阅读量(仅用作展示)")
@QueryField(type = QueryType.EQ)
private Integer virtualViews;
@Schema(description = "实际阅读量")
@QueryField(type = QueryType.EQ)
private Integer actualViews;
@Schema(description = "评分")
@QueryField(type = QueryType.EQ)
private BigDecimal rate;
@Schema(description = "列表显示方式(10小图展示 20大图展示)")
@QueryField(type = QueryType.EQ)
private Integer showType;
@Schema(description = "访问密码")
private String password;
@Schema(description = "可见类型 0所有人 1登录可见 2密码可见")
@QueryField(type = QueryType.EQ)
private Integer permission;
@Schema(description = "发布来源客户端 (APP、H5、小程序等)")
private String platform;
@Schema(description = "文章附件")
private String files;
@Schema(description = "视频地址")
private String video;
@Schema(description = "接受的文件类型")
private String accept;
@Schema(description = "经度")
private String longitude;
@Schema(description = "纬度")
private String latitude;
@Schema(description = "所在省份")
private String province;
@Schema(description = "所在城市")
private String city;
@Schema(description = "所在辖区")
private String region;
@Schema(description = "街道地址")
private String address;
@Schema(description = "点赞数")
@QueryField(type = QueryType.EQ)
private Integer likes;
@Schema(description = "评论数")
@QueryField(type = QueryType.EQ)
private Integer commentNumbers;
@Schema(description = "提醒谁看")
private String toUsers;
@Schema(description = "作者")
private String author;
@Schema(description = "推荐")
@QueryField(type = QueryType.EQ)
private Integer recommend;
@Schema(description = "报名人数")
@QueryField(type = QueryType.EQ)
private Integer bmUsers;
@Schema(description = "用户ID")
@QueryField(type = QueryType.EQ)
private Integer userId;
@Schema(description = "商户ID")
@QueryField(type = QueryType.EQ)
private Long merchantId;
@Schema(description = "项目ID")
@QueryField(type = QueryType.EQ)
private Integer projectId;
@Schema(description = "语言")
private String lang;
@Schema(description = "关联默认语言的文章ID")
@QueryField(type = QueryType.EQ)
private Integer langArticleId;
@Schema(description = "是否自动翻译")
@QueryField(type = QueryType.EQ)
private Boolean translation;
@Schema(description = "编辑器类型 0 Markdown编辑器 1 富文本编辑器 ")
@QueryField(type = QueryType.EQ)
private Boolean editor;
@Schema(description = "pdf文件地址")
private String pdfUrl;
@Schema(description = "版本号")
@QueryField(type = QueryType.EQ)
private Integer version;
@Schema(description = "排序(数字越小越靠前)")
@QueryField(type = QueryType.EQ)
private Integer sortNumber;
@Schema(description = "备注")
private String comments;
@Schema(description = "状态, 0已发布, 1待审核 2已驳回 3违规内容")
@QueryField(type = QueryType.EQ)
private Integer status;
@Schema(description = "是否删除, 0否, 1是")
@QueryField(type = QueryType.EQ)
private Integer deleted;
}

42
src/main/java/com/gxwebsoft/shop/service/ShopArticleService.java

@ -0,0 +1,42 @@
package com.gxwebsoft.shop.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.shop.entity.ShopArticle;
import com.gxwebsoft.shop.param.ShopArticleParam;
import java.util.List;
/**
* 商品文章Service
*
* @author 科技小王子
* @since 2025-08-13 00:28:23
*/
public interface ShopArticleService extends IService<ShopArticle> {
/**
* 分页关联查询
*
* @param param 查询参数
* @return PageResult<ShopArticle>
*/
PageResult<ShopArticle> pageRel(ShopArticleParam param);
/**
* 关联查询全部
*
* @param param 查询参数
* @return List<ShopArticle>
*/
List<ShopArticle> listRel(ShopArticleParam param);
/**
* 根据id查询
*
* @param articleId 文章ID
* @return ShopArticle
*/
ShopArticle getByIdRel(Integer articleId);
}

47
src/main/java/com/gxwebsoft/shop/service/impl/ShopArticleServiceImpl.java

@ -0,0 +1,47 @@
package com.gxwebsoft.shop.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.shop.mapper.ShopArticleMapper;
import com.gxwebsoft.shop.service.ShopArticleService;
import com.gxwebsoft.shop.entity.ShopArticle;
import com.gxwebsoft.shop.param.ShopArticleParam;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 商品文章Service实现
*
* @author 科技小王子
* @since 2025-08-13 00:28:23
*/
@Service
public class ShopArticleServiceImpl extends ServiceImpl<ShopArticleMapper, ShopArticle> implements ShopArticleService {
@Override
public PageResult<ShopArticle> pageRel(ShopArticleParam param) {
PageParam<ShopArticle, ShopArticleParam> page = new PageParam<>(param);
page.setDefaultOrder("sort_number asc, create_time desc");
List<ShopArticle> list = baseMapper.selectPageRel(page, param);
return new PageResult<>(list, page.getTotal());
}
@Override
public List<ShopArticle> listRel(ShopArticleParam param) {
List<ShopArticle> list = baseMapper.selectListRel(param);
// 排序
PageParam<ShopArticle, ShopArticleParam> page = new PageParam<>();
page.setDefaultOrder("sort_number asc, create_time desc");
return page.sortRecords(list);
}
@Override
public ShopArticle getByIdRel(Integer articleId) {
ShopArticleParam param = new ShopArticleParam();
param.setArticleId(articleId);
return param.getOne(baseMapper.selectListRel(param));
}
}

88
src/test/java/com/gxwebsoft/generator/CmsGenerator.java

@ -13,6 +13,11 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
/**
* CMS模块-代码生成工具
@ -56,7 +61,7 @@ public class CmsGenerator {
// "cms_docs_book",
// "cms_docs_content",
// "cms_ad",
"cms_ad_record",
// "cms_ad_record",
// "cms_navigation",
// "cms_design",
// "cms_design_record",
@ -315,6 +320,87 @@ public class CmsGenerator {
mpg.setCfg(cfg);
mpg.execute();
// 自动更新 app.config.ts
updateAppConfig(TABLE_NAMES, MODULE_NAME);
}
/**
* 自动更新 app.config.ts 文件添加新生成的页面路径
*/
private static void updateAppConfig(String[] tableNames, String moduleName) {
String appConfigPath = OUTPUT_LOCATION_UNIAPP + OUTPUT_DIR_VUE + "/app.config.ts";
try {
// 读取原文件内容
String content = new String(Files.readAllBytes(Paths.get(appConfigPath)));
// 为每个表生成页面路径
StringBuilder newPages = new StringBuilder();
for (String tableName : tableNames) {
String entityPath = tableName.replaceAll("_", "");
// 转换为驼峰命名
String[] parts = tableName.split("_");
StringBuilder camelCase = new StringBuilder(parts[0]);
for (int i = 1; i < parts.length; i++) {
camelCase.append(parts[i].substring(0, 1).toUpperCase()).append(parts[i].substring(1));
}
entityPath = camelCase.toString();
newPages.append(" '").append(entityPath).append("/index',\n");
newPages.append(" '").append(entityPath).append("/add',\n");
}
// 查找对应模块的子包配置
String modulePattern = "\"root\":\\s*\"" + moduleName + "\",\\s*\"pages\":\\s*\\[([^\\]]*)]";
Pattern pattern = Pattern.compile(modulePattern, Pattern.DOTALL);
Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
String existingPages = matcher.group(1);
// 检查页面是否已存在,避免重复添加
boolean needUpdate = false;
String[] newPageArray = newPages.toString().split("\n");
for (String newPage : newPageArray) {
if (!newPage.trim().isEmpty() && !existingPages.contains(newPage.trim().replace(" ", "").replace(",", ""))) {
needUpdate = true;
break;
}
}
if (needUpdate) {
// 备份原文件
String backupPath = appConfigPath + ".backup." + System.currentTimeMillis();
Files.copy(Paths.get(appConfigPath), Paths.get(backupPath));
System.out.println("已备份原文件到: " + backupPath);
// 在现有页面列表末尾添加新页面
String updatedPages = existingPages.trim();
if (!updatedPages.endsWith(",")) {
updatedPages += ",";
}
updatedPages += "\n" + newPages.toString().trim();
// 替换内容
String updatedContent = content.replace(matcher.group(1), updatedPages);
// 写入更新后的内容
Files.write(Paths.get(appConfigPath), updatedContent.getBytes());
System.out.println("✅ 已自动更新 app.config.ts,添加了以下页面路径:");
System.out.println(newPages.toString());
} else {
System.out.println("ℹ️ app.config.ts 中已包含所有页面路径,无需更新");
}
} else {
System.out.println("⚠️ 未找到 " + moduleName + " 模块的子包配置,请手动添加页面路径");
}
} catch (Exception e) {
System.err.println("❌ 更新 app.config.ts 失败: " + e.getMessage());
e.printStackTrace();
}
}
}

87
src/test/java/com/gxwebsoft/generator/ShopGenerator.java

@ -13,6 +13,11 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
/**
* CMS模块-代码生成工具
@ -100,6 +105,7 @@ public class ShopGenerator {
// "shop_express_template",
// "shop_express_template_detail",
// "shop_gift"
"shop_article"
};
// 需要去除的表前缀
private static final String[] TABLE_PREFIX = new String[]{
@ -343,6 +349,87 @@ public class ShopGenerator {
mpg.setCfg(cfg);
mpg.execute();
// 自动更新 app.config.ts
updateAppConfig(TABLE_NAMES, MODULE_NAME);
}
/**
* 自动更新 app.config.ts 文件添加新生成的页面路径
*/
private static void updateAppConfig(String[] tableNames, String moduleName) {
String appConfigPath = OUTPUT_LOCATION_UNIAPP + OUTPUT_DIR_VUE + "/app.config.ts";
try {
// 读取原文件内容
String content = new String(Files.readAllBytes(Paths.get(appConfigPath)));
// 为每个表生成页面路径
StringBuilder newPages = new StringBuilder();
for (String tableName : tableNames) {
String entityPath = tableName.replaceAll("_", "");
// 转换为驼峰命名
String[] parts = tableName.split("_");
StringBuilder camelCase = new StringBuilder(parts[0]);
for (int i = 1; i < parts.length; i++) {
camelCase.append(parts[i].substring(0, 1).toUpperCase()).append(parts[i].substring(1));
}
entityPath = camelCase.toString();
newPages.append(" '").append(entityPath).append("/index',\n");
newPages.append(" '").append(entityPath).append("/add',\n");
}
// 查找对应模块的子包配置
String modulePattern = "\"root\":\\s*\"" + moduleName + "\",\\s*\"pages\":\\s*\\[([^\\]]*)]";
Pattern pattern = Pattern.compile(modulePattern, Pattern.DOTALL);
Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
String existingPages = matcher.group(1);
// 检查页面是否已存在,避免重复添加
boolean needUpdate = false;
String[] newPageArray = newPages.toString().split("\n");
for (String newPage : newPageArray) {
if (!newPage.trim().isEmpty() && !existingPages.contains(newPage.trim().replace(" ", "").replace(",", ""))) {
needUpdate = true;
break;
}
}
if (needUpdate) {
// 备份原文件
String backupPath = appConfigPath + ".backup." + System.currentTimeMillis();
Files.copy(Paths.get(appConfigPath), Paths.get(backupPath));
System.out.println("已备份原文件到: " + backupPath);
// 在现有页面列表末尾添加新页面
String updatedPages = existingPages.trim();
if (!updatedPages.endsWith(",")) {
updatedPages += ",";
}
updatedPages += "\n" + newPages.toString().trim();
// 替换内容
String updatedContent = content.replace(matcher.group(1), updatedPages);
// 写入更新后的内容
Files.write(Paths.get(appConfigPath), updatedContent.getBytes());
System.out.println("✅ 已自动更新 app.config.ts,添加了以下页面路径:");
System.out.println(newPages.toString());
} else {
System.out.println("ℹ️ app.config.ts 中已包含所有页面路径,无需更新");
}
} else {
System.out.println("⚠️ 未找到 " + moduleName + " 模块的子包配置,请手动添加页面路径");
}
} catch (Exception e) {
System.err.println("❌ 更新 app.config.ts 失败: " + e.getMessage());
e.printStackTrace();
}
}
}

2
src/test/java/com/gxwebsoft/generator/templates/add.config.ts.btl

@ -1,4 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '新增${table.comment!}',
navigationBarTitleText: '新增${table.comment!'数据'}',
navigationBarTextStyle: 'black'
})

12
src/test/java/com/gxwebsoft/generator/templates/add.tsx.btl

@ -36,7 +36,7 @@ const Add${entity} = () => {
}
Taro.showToast({
title: `${params.id ? '更新' : '保存'}成功`,
title: `操作成功`,
icon: 'success'
})
@ -45,7 +45,7 @@ const Add${entity} = () => {
}, 1000)
} catch (error) {
Taro.showToast({
title: `${params.id ? '更新' : '保存'}失败`,
title: `操作失败`,
icon: 'error'
});
}
@ -97,11 +97,11 @@ const Add${entity} = () => {
<CellGroup style={{padding: '4px 0'}}>
<% for(field in table.fields){ %>
<% if(field.propertyName != 'id' && field.propertyName != 'createTime' && field.propertyName != 'updateTime'){ %>
<Form.Item name="${field.propertyName}" label="${field.comment!}" initialValue={FormData.${field.propertyName}} required>
<% if(field.propertyType == 'String' && (field.comment?contains('描述') || field.comment?contains('备注') || field.comment?contains('内容'))){ %>
<TextArea maxLength={200} placeholder="请输入${field.comment!}"/>
<Form.Item name="${field.propertyName}" label="${field.comment!field.propertyName}" initialValue={FormData.${field.propertyName}} required>
<% if(field.propertyType == 'String' && field.comment?? && (field.comment?contains('描述') || field.comment?contains('备注') || field.comment?contains('内容'))){ %>
<TextArea maxLength={200} placeholder="请输入${field.comment!'内容'}"/>
<% } else { %>
<Input placeholder="请输入${field.comment!}" maxLength={50}/>
<Input placeholder="请输入${field.comment!'字段'}" maxLength={50}/>
<% } %>
</Form.Item>
<% } %>

8
src/test/java/com/gxwebsoft/generator/templates/controller.java.btl

@ -131,11 +131,19 @@ public class ${table.controllerName} {
<% } %>
@PostMapping()
public ApiResult<?> save(@RequestBody ${entity} ${table.entityPath}) {
<% var hasUserIdField = false; %>
<% for(field in table.fields){ %>
<% if(field.propertyName == 'userId'){ %>
<% hasUserIdField = true; %>
<% } %>
<% } %>
<% if(hasUserIdField){ %>
// 记录当前登录用户id
User loginUser = getLoginUser();
if (loginUser != null) {
${table.entityPath}.setUserId(loginUser.getUserId());
}
<% } %>
if (${serviceIns}.save(${table.entityPath})) {
return success("添加成功");
}

2
src/test/java/com/gxwebsoft/generator/templates/index.config.ts.btl

@ -1,4 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '${table.comment!}管理',
navigationBarTitleText: '${table.comment!'数据'}管理',
navigationBarTextStyle: 'black'
})

52
src/test/java/com/gxwebsoft/generator/templates/index.tsx.btl

@ -8,7 +8,15 @@ import {list${entity}, remove${entity}, update${entity}} from "@/api/${package.M
const ${entity}List = () => {
const [list, setList] = useState<${entity}[]>([])
<% var hasIsDefaultField = false; %>
<% for(field in table.fields){ %>
<% if(field.propertyName == 'isDefault'){ %>
<% hasIsDefaultField = true; %>
<% } %>
<% } %>
<% if(hasIsDefaultField){ %>
const [selectedItem, setSelectedItem] = useState<${entity}>()
<% } %>
const reload = () => {
list${entity}({
@ -16,8 +24,10 @@ const ${entity}List = () => {
})
.then(data => {
setList(data || [])
<% if(hasIsDefaultField){ %>
// 设置默认选中项
setSelectedItem(data.find(item => item.isDefault))
<% } %>
})
.catch(() => {
Taro.showToast({
@ -27,6 +37,7 @@ const ${entity}List = () => {
})
}
<% if(hasIsDefaultField){ %>
const onDefault = async (item: ${entity}) => {
if (selectedItem) {
await update${entity}({
@ -45,15 +56,6 @@ const ${entity}List = () => {
reload();
}
const onDel = async (id?: number) => {
await remove${entity}(id)
Taro.showToast({
title: '删除成功',
icon: 'success'
});
reload();
}
const selectItem = async (item: ${entity}) => {
if (selectedItem) {
await update${entity}({
@ -69,6 +71,18 @@ const ${entity}List = () => {
Taro.navigateBack()
},500)
}
<% } %>
const onDel = async (id?: number) => {
await remove${entity}(id)
Taro.showToast({
title: '删除成功',
icon: 'success'
});
reload();
}
useDidShow(() => {
reload()
@ -98,22 +112,38 @@ const ${entity}List = () => {
<>
{list.map((item, _) => (
<Cell.Group key={item.id}>
<% if(hasIsDefaultField){ %>
<Cell className={'flex flex-col gap-1'} onClick={() => selectItem(item)}>
<% } else { %>
<Cell className={'flex flex-col gap-1'}>
<% } %>
<View>
<View className={'font-medium text-sm'}>{item.name}</View>
<% var displayFields = []; %>
<% for(field in table.fields){ %>
<% if(field.propertyName != 'id' && field.propertyName != 'createTime' && field.propertyName != 'updateTime' && field.propertyName != 'isDefault'){ %>
<% displayFields.add(field); %>
<% } %>
<% } %>
<% if(displayFields.size() > 0){ %>
<View className={'font-medium text-sm'}>{item.${displayFields[0].propertyName}}</View>
<% } %>
</View>
<% if(displayFields.size() > 1){ %>
<View className={'text-xs'}>
{item.description}
{item.${displayFields[1].propertyName}}
</View>
<% } %>
</Cell>
<Cell
align="center"
<% if(hasIsDefaultField){ %>
title={
<View className={'flex items-center gap-1'} onClick={() => onDefault(item)}>
{item.isDefault ? <Checked className={'text-green-600'} size={16}/> : <CheckNormal size={16}/>}
<View className={'text-gray-400'}>默认选项</View>
</View>
}
<% } %>
extra={
<>
<View className={'text-gray-400'} onClick={() => onDel(item.id)}>

Loading…
Cancel
Save