-- Business blueprint Velocity templates
-- Import this script after sql/db.sql.
-- Template ids:
-- 9101 backend: RuoYi Spring Boot + MyBatis
-- 9102 admin_frontend: Vue2 + Element UI for RuoYi admin
-- 9103 frontend: Vue3 + Element Plus portal

SET FOREIGN_KEY_CHECKS = 0;

DELETE FROM sys_project_template WHERE template_id IN (9101, 9102, 9103);
DELETE FROM sys_project_structure WHERE template_id IN (9101, 9102, 9103) OR node_id BETWEEN 910100 AND 910399;
DELETE FROM sys_template_file WHERE template_id IN (9101, 9102, 9103) OR template_file_id BETWEEN 910100 AND 910399;
DELETE FROM sys_template WHERE template_id IN (9101, 9102, 9103);

INSERT INTO sys_template (template_id, template_name, template_path, template_desc, template_type, template_status, create_by, create_time, remark) VALUES
(9101, 'Business Blueprint Backend', '/blueprint/backend', 'RuoYi backend with CRUD and executable blueprint effects', 'backend', 0, 'admin', sysdate(), 'business blueprint'),
(9102, 'Business Blueprint Admin Frontend', '/blueprint/admin_frontend', 'Vue2 Element UI admin pages with blueprint action buttons', 'admin_frontend', 0, 'admin', sysdate(), 'business blueprint'),
(9103, 'Business Blueprint Portal Frontend', '/blueprint/frontend', 'Vue3 Element Plus portal pages with blueprint action buttons', 'frontend', 0, 'admin', sysdate(), 'business blueprint');

-- Backend template files
INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES
(910101, 9101, 'pom.xml.vm', NULL, 'pom.xml.vm',
'<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>${packageName}</groupId>
  <artifactId>${projectFileName}-backend</artifactId>
  <version>1.0.0</version>
  <name>${projectName}</name>
  <description>Generated backend with business blueprint actions</description>

  <parent>
    <groupId>com.ruoyi</groupId>
    <artifactId>ruoyi</artifactId>
    <version>3.8.8</version>
  </parent>

  <dependencies>
    <dependency>
      <groupId>com.ruoyi</groupId>
      <artifactId>ruoyi-common</artifactId>
    </dependency>
    <dependency>
      <groupId>com.ruoyi</groupId>
      <artifactId>ruoyi-framework</artifactId>
    </dependency>
  </dependencies>
</project>
', 'admin', sysdate()),
(910102, 9101, 'application.yml.vm', NULL, 'application.yml.vm',
'server:
  port: 8080

spring:
  application:
    name: ${projectFileName}-backend
', 'admin', sysdate()),
(910103, 9101, 'README.md.vm', NULL, 'README.md.vm',
'# ${projectName} Backend

Generated from EasyCode business blueprint templates.

Create table SQL:

```sql
${createTableSql}
```

# Business Actions

#if($hasBusinessActions)
#foreach($action in $businessActions)
- ${action.name} `${action.code}` `${action.method} ${action.path}` owner `${action.ownerTable}`
#end
#else
No business actions in blueprint.
#end
', 'admin', sysdate()),
(910104, 9101, 'domain.java.vm', NULL, 'domain.java.vm',
'package ${packageName}.domain;

import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

/**
 * ${functionName} object ${tableName}
 *
 * @author ${author}
 * @date ${datetime}
 */
public class ${ClassName} extends BaseEntity
{
    private static final long serialVersionUID = 1L;

#foreach($column in $columns)
#if(!$table.isSuperColumn($column.javaField))
    /** ${column.columnComment} */
#if($column.javaType == "Date")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
#end
#if($column.isList == "1")
    @Excel(name = "${column.columnComment}")
#end
    private ${column.javaType} ${column.javaField};

#end
#end
#foreach($column in $columns)
#if(!$table.isSuperColumn($column.javaField))
    public void set${column.capJavaField}(${column.javaType} ${column.javaField})
    {
        this.${column.javaField} = ${column.javaField};
    }

    public ${column.javaType} get${column.capJavaField}()
    {
        return ${column.javaField};
    }

#end
#end
    @Override
    public String toString()
    {
        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
#foreach($column in $columns)
#if(!$table.isSuperColumn($column.javaField))
            .append("${column.javaField}", get${column.capJavaField}())
#end
#end
            .toString();
    }
}
', 'admin', sysdate()),
(910105, 9101, 'mapper.java.vm', NULL, 'mapper.java.vm',
'package ${packageName}.mapper;

import java.util.List;
import ${packageName}.domain.${ClassName};

/**
 * ${functionName} Mapper
 */
public interface ${ClassName}Mapper
{
    public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField});

    public List<${ClassName}> select${ClassName}List(${ClassName} ${className});

    public int insert${ClassName}(${ClassName} ${className});

    public int update${ClassName}(${ClassName} ${className});

    public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField});

    public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s);
}
', 'admin', sysdate()),
(910106, 9101, 'mapper.xml.vm', NULL, 'mapper.xml.vm',
'<?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="${packageName}.mapper.${ClassName}Mapper">

    <resultMap type="${packageName}.domain.${ClassName}" id="${ClassName}Result">
#foreach($column in $columns)
#if($column.isPk == "1")
        <id property="${column.javaField}" column="${column.columnName}" />
#else
        <result property="${column.javaField}" column="${column.columnName}" />
#end
#end
    </resultMap>

    <sql id="select${ClassName}Vo">
        select
#foreach($column in $columns)
            ${column.columnName}#if($foreach.hasNext),#end
#end
        from ${tableName}
    </sql>

    <select id="select${ClassName}List" parameterType="${packageName}.domain.${ClassName}" resultMap="${ClassName}Result">
        <include refid="select${ClassName}Vo"/>
        <where>
#foreach($column in $columns)
#if($column.isQuery == "1")
            <if test="${column.javaField} != null"> and ${column.columnName} = #{${column.javaField}}</if>
#end
#end
        </where>
    </select>

    <select id="select${ClassName}By${pkColumn.capJavaField}" parameterType="${pkColumn.javaType}" resultMap="${ClassName}Result">
        <include refid="select${ClassName}Vo"/>
        where ${pkColumn.columnName} = #{${pkColumn.javaField}}
    </select>

    <insert id="insert${ClassName}" parameterType="${packageName}.domain.${ClassName}"#if($pkColumn.increment) useGeneratedKeys="true" keyProperty="${pkColumn.javaField}"#end>
        insert into ${tableName}
        <trim prefix="(" suffix=")" suffixOverrides=",">
#foreach($column in $columns)
#if($column.isInsert == "1" && $column.isIncrement != "1")
            <if test="${column.javaField} != null">${column.columnName},</if>
#end
#end
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
#foreach($column in $columns)
#if($column.isInsert == "1" && $column.isIncrement != "1")
            <if test="${column.javaField} != null">#{${column.javaField}},</if>
#end
#end
        </trim>
    </insert>

    <update id="update${ClassName}" parameterType="${packageName}.domain.${ClassName}">
        update ${tableName}
        <trim prefix="SET" suffixOverrides=",">
#foreach($column in $columns)
#if($column.isEdit == "1" && $column.isPk != "1")
            <if test="${column.javaField} != null">${column.columnName} = #{${column.javaField}},</if>
#end
#end
        </trim>
        where ${pkColumn.columnName} = #{${pkColumn.javaField}}
    </update>

    <delete id="delete${ClassName}By${pkColumn.capJavaField}" parameterType="${pkColumn.javaType}">
        delete from ${tableName} where ${pkColumn.columnName} = #{${pkColumn.javaField}}
    </delete>

    <delete id="delete${ClassName}By${pkColumn.capJavaField}s" parameterType="String">
        delete from ${tableName} where ${pkColumn.columnName} in
        <foreach item="${pkColumn.javaField}" collection="array" open="(" separator="," close=")">
            #{${pkColumn.javaField}}
        </foreach>
    </delete>
</mapper>
', 'admin', sysdate()),
(910107, 9101, 'service.java.vm', NULL, 'service.java.vm',
'package ${packageName}.service;

import java.util.List;
import java.util.Map;
import ${packageName}.domain.${ClassName};

/**
 * ${functionName} Service
 */
public interface I${ClassName}Service
{
    public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField});

    public List<${ClassName}> select${ClassName}List(${ClassName} ${className});

    public int insert${ClassName}(${ClassName} ${className});

    public int update${ClassName}(${ClassName} ${className});

    public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s);

    public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField});

#if($hasTableBusinessActions)
#foreach($action in $tableBusinessActions)
    public int ${action.code}(Map<String, Object> params);

#end
#end
}
', 'admin', sysdate()),
(910108, 9101, 'serviceImpl.java.vm', NULL, 'serviceImpl.java.vm',
'package ${packageName}.service.impl;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.exception.ServiceException;
import ${packageName}.domain.${ClassName};
import ${packageName}.mapper.${ClassName}Mapper;
import ${packageName}.service.I${ClassName}Service;

/**
 * ${functionName} Service implementation
 */
@Service
public class ${ClassName}ServiceImpl implements I${ClassName}Service
{
    @Resource
    private ${ClassName}Mapper ${className}Mapper;
#if($hasTableBusinessActions)
    @Resource
    private JdbcTemplate jdbcTemplate;
#end

    @Override
    public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField})
    {
        return ${className}Mapper.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField});
    }

    @Override
    public List<${ClassName}> select${ClassName}List(${ClassName} ${className})
    {
        return ${className}Mapper.select${ClassName}List(${className});
    }

    @Override
    public int insert${ClassName}(${ClassName} ${className})
    {
        return ${className}Mapper.insert${ClassName}(${className});
    }

    @Override
    public int update${ClassName}(${ClassName} ${className})
    {
        return ${className}Mapper.update${ClassName}(${className});
    }

    @Override
    public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s)
    {
        return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s);
    }

    @Override
    public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField})
    {
        return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField});
    }

#if($hasTableBusinessActions)
#foreach($action in $tableBusinessActions)
    /**
     * ${action.name}
#if($action.requestFields)
     * Request fields:
#foreach($field in $action.requestFields)
     * - ${field}
#end
#end
#if($action.rules)
     * Rules:
#foreach($rule in $action.rules)
     * - ${rule}
#end
#end
#if($action.effects)
     * Effects:
#foreach($effect in $action.effects)
     * - ${effect.type} ${effect.targetTable}.${effect.targetField} ${effect.value} ${effect.description}
#end
#end
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public int ${action.code}(Map<String, Object> params)
    {
        if (params == null) {
            throw new ServiceException("业务动作参数不能为空");
        }
#if($action.requestFields)
#foreach($field in $action.requestFields)
        requireBusinessParam(params, "${field}");
#end
#end
 #if($action.ruleChecks)
#foreach($rule in $action.ruleChecks)
#set($conditionFields = $rule.conditionFields)
#if($rule.type == "EXISTS")
        assertExists(params, "${rule.targetTable}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${rule.message}");
#elseif($rule.type == "NOT_EXISTS")
        assertNotExists(params, "${rule.targetTable}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${rule.message}");
#elseif($rule.type == "FIELD_EQUALS")
        assertFieldEquals(params, "${rule.targetTable}", "${rule.targetField}", "${rule.expectedValue}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${rule.message}");
#elseif($rule.type == "FIELD_NOT_EQUALS")
        assertFieldNotEquals(params, "${rule.targetTable}", "${rule.targetField}", "${rule.expectedValue}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${rule.message}");
#elseif($rule.type == "FIELD_IN")
        assertFieldIn(params, "${rule.targetTable}", "${rule.targetField}", new String[] {#foreach($value in $rule.expectedValues)"${value}"#if($foreach.hasNext), #end#end}, new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${rule.message}");
#elseif($rule.type == "NUMBER_GTE")
        assertNumberGte(params, "${rule.targetTable}", "${rule.targetField}", "${rule.compareValue}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${rule.message}");
#end
#end
#end
        int rows = 0;
#if($action.effects)
#foreach($effect in $action.effects)
#if($effect.type == "UPDATE_FIELD")
#set($conditionFields = $effect.conditionFields)
#if(!$conditionFields || $conditionFields.size() == 0)
#set($conditionFields = $action.requestFields)
#end
        rows += executeUpdateFieldEffect(params, "${effect.targetTable}", "${effect.targetField}", "${effect.value}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end});
#elseif($effect.type == "INSERT_ROW")
#set($effectIndex = $foreach.index)
        Map<String, Object> insertValues${effectIndex} = new java.util.LinkedHashMap<String, Object>();
#foreach($entry in $effect.values.entrySet())
        insertValues${effectIndex}.put("${entry.key}", resolveBusinessValue(params, "${entry.value}"));
#end
        rows += insertRow("${effect.targetTable}", insertValues${effectIndex});
#elseif($effect.type == "UPDATE_FIELDS")
#set($conditionFields = $effect.conditionFields)
#set($effectIndex = $foreach.index)
        Map<String, Object> updateValues${effectIndex} = new java.util.LinkedHashMap<String, Object>();
#foreach($entry in $effect.values.entrySet())
        updateValues${effectIndex}.put("${entry.key}", resolveBusinessValue(params, "${entry.value}"));
#end
        rows += updateFields(params, "${effect.targetTable}", updateValues${effectIndex}, new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end});
#elseif($effect.type == "SET_STATUS")
#set($conditionFields = $effect.conditionFields)
        rows += setStatus(params, "${effect.targetTable}", "${effect.targetField}", resolveBusinessValue(params, "${effect.value}"), new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end});
#elseif($effect.type == "INCREASE_NUMBER")
#set($conditionFields = $effect.conditionFields)
        rows += increaseNumberField(params, "${effect.targetTable}", "${effect.targetField}", "${effect.amount}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end});
#elseif($effect.type == "DECREASE_NUMBER")
#set($conditionFields = $effect.conditionFields)
        rows += decreaseNumberField(params, "${effect.targetTable}", "${effect.targetField}", "${effect.amount}", "${effect.minValue}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${effect.message}");
#else
        // Unsupported effect type ${effect.type}: ${effect.description}
#end
#end
#end
        return rows;
    }

#end
    private void requireBusinessParam(Map<String, Object> params, String field)
    {
        if (!params.containsKey(field) || params.get(field) == null) {
            throw new ServiceException("缺少业务动作参数：" + field);
        }
    }

    private void assertExists(Map<String, Object> params, String tableName, String[] conditionFields, String message)
    {
        if (countRows(params, tableName, conditionFields) <= 0) {
            throw new ServiceException(defaultMessage(message, "Business record does not exist"));
        }
    }

    private void assertNotExists(Map<String, Object> params, String tableName, String[] conditionFields, String message)
    {
        if (countRows(params, tableName, conditionFields) > 0) {
            throw new ServiceException(defaultMessage(message, "Business record already exists"));
        }
    }

    private void assertFieldEquals(Map<String, Object> params, String tableName, String targetField, String expectedValue,
                                   String[] conditionFields, String message)
    {
        Object value = querySingleField(params, tableName, targetField, conditionFields);
        if (value == null || !String.valueOf(resolveBusinessValue(params, expectedValue)).equals(String.valueOf(value))) {
            throw new ServiceException(defaultMessage(message, "Business rule check failed"));
        }
    }

    private void assertFieldNotEquals(Map<String, Object> params, String tableName, String targetField, String expectedValue,
                                      String[] conditionFields, String message)
    {
        Object value = querySingleField(params, tableName, targetField, conditionFields);
        if (value != null && String.valueOf(resolveBusinessValue(params, expectedValue)).equals(String.valueOf(value))) {
            throw new ServiceException(defaultMessage(message, "Business rule check failed"));
        }
    }

    private void assertFieldIn(Map<String, Object> params, String tableName, String targetField, String[] expectedValues,
                               String[] conditionFields, String message)
    {
        Object value = querySingleField(params, tableName, targetField, conditionFields);
        String current = value == null ? "" : String.valueOf(value);
        for (String expectedValue : expectedValues) {
            if (String.valueOf(resolveBusinessValue(params, expectedValue)).equals(current)) {
                return;
            }
        }
        throw new ServiceException(defaultMessage(message, "Business rule check failed"));
    }

    private void assertNumberGte(Map<String, Object> params, String tableName, String targetField, String compareValue,
                                 String[] conditionFields, String message)
    {
        Object value = querySingleField(params, tableName, targetField, conditionFields);
        BigDecimal current = toBigDecimal(value, "Business numeric value is invalid");
        BigDecimal expected = resolveBusinessNumber(params, compareValue);
        if (current.compareTo(expected) < 0) {
            throw new ServiceException(defaultMessage(message, "Business numeric rule check failed"));
        }
    }

    private int insertRow(String tableName, Map<String, Object> values)
    {
        String safeTableName = requireSafeIdentifier(tableName, "Business effect target table is invalid");
        if (values == null || values.isEmpty()) {
            throw new ServiceException("Business insert values cannot be empty");
        }
        StringBuilder fields = new StringBuilder();
        StringBuilder placeholders = new StringBuilder();
        List<Object> args = new ArrayList<Object>();
        for (Map.Entry<String, Object> entry : values.entrySet()) {
            if (fields.length() > 0) {
                fields.append(", ");
                placeholders.append(", ");
            }
            fields.append(requireSafeIdentifier(entry.getKey(), "Business effect value field is invalid"));
            placeholders.append("?");
            args.add(entry.getValue());
        }
        return jdbcTemplate.update("insert into " + safeTableName + " (" + fields + ") values (" + placeholders + ")", args.toArray());
    }

    private int updateFields(Map<String, Object> params, String tableName, Map<String, Object> values, String[] conditionFields)
    {
        String safeTableName = requireSafeIdentifier(tableName, "Business effect target table is invalid");
        if (values == null || values.isEmpty()) {
            throw new ServiceException("Business update values cannot be empty");
        }
        StringBuilder sets = new StringBuilder();
        List<Object> args = new ArrayList<Object>();
        for (Map.Entry<String, Object> entry : values.entrySet()) {
            if (sets.length() > 0) {
                sets.append(", ");
            }
            sets.append(requireSafeIdentifier(entry.getKey(), "Business effect value field is invalid")).append(" = ?");
            args.add(entry.getValue());
        }
        String whereClause = buildWhereClause(params, conditionFields);
        appendConditionValues(args, params, conditionFields);
        return jdbcTemplate.update("update " + safeTableName + " set " + sets + " where " + whereClause, args.toArray());
    }

    private int setStatus(Map<String, Object> params, String tableName, String targetField, Object value, String[] conditionFields)
    {
        Map<String, Object> values = new java.util.LinkedHashMap<String, Object>();
        values.put(targetField, value);
        return updateFields(params, tableName, values, conditionFields);
    }

    private int increaseNumberField(Map<String, Object> params, String tableName, String targetField, String amountExpression,
                                    String[] conditionFields)
    {
        String safeTableName = requireSafeIdentifier(tableName, "Business effect target table is invalid");
        String safeTargetField = requireSafeIdentifier(targetField, "Business effect target field is invalid");
        BigDecimal amount = resolveBusinessNumber(params, amountExpression);
        String whereClause = buildWhereClause(params, conditionFields);
        List<Object> args = new ArrayList<Object>();
        args.add(amount);
        appendConditionValues(args, params, conditionFields);
        return jdbcTemplate.update("update " + safeTableName + " set " + safeTargetField + " = " + safeTargetField + " + ? where " + whereClause, args.toArray());
    }

    private int decreaseNumberField(Map<String, Object> params, String tableName, String targetField, String amountExpression,
                                    String minValueExpression, String[] conditionFields, String message)
    {
        String safeTableName = requireSafeIdentifier(tableName, "Business effect target table is invalid");
        String safeTargetField = requireSafeIdentifier(targetField, "Business effect target field is invalid");
        BigDecimal amount = resolveBusinessNumber(params, amountExpression);
        BigDecimal minValue = resolveBusinessNumber(params, minValueExpression == null || minValueExpression.length() == 0 ? "0" : minValueExpression);
        String whereClause = buildWhereClause(params, conditionFields);
        List<Object> args = new ArrayList<Object>();
        args.add(amount);
        appendConditionValues(args, params, conditionFields);
        args.add(amount);
        args.add(minValue);
        int rows = jdbcTemplate.update("update " + safeTableName + " set " + safeTargetField + " = " + safeTargetField + " - ? where " + whereClause + " and " + safeTargetField + " - ? >= ?", args.toArray());
        if (rows == 0) {
            throw new ServiceException(defaultMessage(message, "Business numeric decrease failed"));
        }
        return rows;
    }

    private int executeUpdateFieldEffect(Map<String, Object> params, String tableName, String targetField,
                                         String valueExpression, String[] conditionFields)
    {
        String safeTableName = requireSafeIdentifier(tableName, "业务影响目标表不合法");
        String safeTargetField = requireSafeIdentifier(targetField, "业务影响目标字段不合法");
        String safeValueExpression = requireSafeExpression(valueExpression);
        String whereClause = buildWhereClause(params, conditionFields);
        Object[] values = conditionValues(params, conditionFields);
        String sql = "update " + safeTableName + " set " + safeTargetField + " = " + safeValueExpression + " where " + whereClause;
        return jdbcTemplate.update(sql, values);
    }

    private String buildWhereClause(Map<String, Object> params, String[] conditionFields)
    {
        if (conditionFields == null || conditionFields.length == 0) {
            throw new ServiceException("业务影响缺少条件字段");
        }
        StringBuilder where = new StringBuilder();
        for (String field : conditionFields) {
            String safeField = requireSafeIdentifier(field, "业务影响条件字段不合法");
            requireBusinessParam(params, safeField);
            if (where.length() > 0) {
                where.append(" and ");
            }
            where.append(safeField).append(" = ?");
        }
        return where.toString();
    }

    private Object[] conditionValues(Map<String, Object> params, String[] conditionFields)
    {
        Object[] values = new Object[conditionFields.length];
        for (int i = 0; i < conditionFields.length; i++) {
            values[i] = params.get(conditionFields[i]);
        }
        return values;
    }

    private int countRows(Map<String, Object> params, String tableName, String[] conditionFields)
    {
        String safeTableName = requireSafeIdentifier(tableName, "Business rule target table is invalid");
        String whereClause = buildWhereClause(params, conditionFields);
        Integer count = jdbcTemplate.queryForObject("select count(1) from " + safeTableName + " where " + whereClause, conditionValues(params, conditionFields), Integer.class);
        return count == null ? 0 : count.intValue();
    }

    private Object querySingleField(Map<String, Object> params, String tableName, String targetField, String[] conditionFields)
    {
        String safeTableName = requireSafeIdentifier(tableName, "Business rule target table is invalid");
        String safeTargetField = requireSafeIdentifier(targetField, "Business rule target field is invalid");
        String whereClause = buildWhereClause(params, conditionFields);
        List<Map<String, Object>> rows = jdbcTemplate.queryForList("select " + safeTargetField + " from " + safeTableName + " where " + whereClause + " limit 1", conditionValues(params, conditionFields));
        if (rows.isEmpty()) {
            return null;
        }
        return rows.get(0).get(safeTargetField);
    }

    private void appendConditionValues(List<Object> args, Map<String, Object> params, String[] conditionFields)
    {
        for (Object value : conditionValues(params, conditionFields)) {
            args.add(value);
        }
    }

    private Object resolveBusinessValue(Map<String, Object> params, String valueExpression)
    {
        if (valueExpression == null) {
            return null;
        }
        if ("#[[${now}]]#".equals(valueExpression)) {
            return new Date();
        }
        if (valueExpression.startsWith("#[[${param.]]#") && valueExpression.endsWith("}")) {
            String field = valueExpression.substring(8, valueExpression.length() - 1);
            requireBusinessParam(params, field);
            return params.get(field);
        }
        return valueExpression;
    }

    private BigDecimal resolveBusinessNumber(Map<String, Object> params, String valueExpression)
    {
        return toBigDecimal(resolveBusinessValue(params, valueExpression), "Business numeric expression is invalid");
    }

    private BigDecimal toBigDecimal(Object value, String message)
    {
        try {
            return new BigDecimal(String.valueOf(value));
        } catch (RuntimeException e) {
            throw new ServiceException(message);
        }
    }

    private String defaultMessage(String message, String fallback)
    {
        return message == null || message.length() == 0 ? fallback : message;
    }

    private String requireSafeIdentifier(String value, String message)
    {
        if (value == null || !value.matches("[A-Za-z][A-Za-z0-9_]*")) {
            throw new ServiceException(message);
        }
        return value;
    }

    private String requireSafeExpression(String valueExpression)
    {
        if (valueExpression == null || !valueExpression.matches("[A-Za-z0-9_ +*/().-]{1,200}")
                || valueExpression.contains(";") || valueExpression.contains("--")) {
            throw new ServiceException("业务影响字段表达式不合法");
        }
        return valueExpression;
    }
#end
}
', 'admin', sysdate()),
(910109, 9101, 'controller.java.vm', NULL, 'controller.java.vm',
'package ${packageName}.controller;

import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import ${packageName}.domain.${ClassName};
import ${packageName}.service.I${ClassName}Service;

/**
 * ${functionName} Controller
 */
@RestController
@RequestMapping("/${moduleName}/${businessName}")
public class ${ClassName}Controller extends BaseController
{
    @Resource
    private I${ClassName}Service ${className}Service;

    @GetMapping("/list")
    public TableDataInfo list(${ClassName} ${className})
    {
        startPage();
        List<${ClassName}> list = ${className}Service.select${ClassName}List(${className});
        return getDataTable(list);
    }

    @PostMapping("/export")
    public void export(HttpServletResponse response, ${ClassName} ${className})
    {
        List<${ClassName}> list = ${className}Service.select${ClassName}List(${className});
        ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class);
        util.exportExcel(response, list, "${functionName}");
    }

    @GetMapping(value = "/{${pkColumn.javaField}}")
    public AjaxResult getInfo(@PathVariable("${pkColumn.javaField}") ${pkColumn.javaType} ${pkColumn.javaField})
    {
        return success(${className}Service.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}));
    }

    @Log(title = "${functionName}", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@RequestBody ${ClassName} ${className})
    {
        return toAjax(${className}Service.insert${ClassName}(${className}));
    }

    @Log(title = "${functionName}", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@RequestBody ${ClassName} ${className})
    {
        return toAjax(${className}Service.update${ClassName}(${className}));
    }

    @Log(title = "${functionName}", businessType = BusinessType.DELETE)
    @DeleteMapping("/{${pkColumn.javaField}s}")
    public AjaxResult remove(@PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s)
    {
        return toAjax(${className}Service.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s));
    }

#if($hasTableBusinessActions)
#foreach($action in $tableBusinessActions)
    @Log(title = "${action.name}", businessType = BusinessType.OTHER)
#if($action.method == "GET")
    @GetMapping("${action.path}")
    public AjaxResult ${action.code}(@RequestParam Map<String, Object> params)
    {
        return toAjax(${className}Service.${action.code}(params));
    }
#elseif($action.method == "PUT")
    @PutMapping("${action.path}")
    public AjaxResult ${action.code}(@RequestBody Map<String, Object> params)
    {
        return toAjax(${className}Service.${action.code}(params));
    }
#elseif($action.method == "DELETE")
    @DeleteMapping("${action.path}")
    public AjaxResult ${action.code}(@RequestBody(required = false) Map<String, Object> params)
    {
        return toAjax(${className}Service.${action.code}(params));
    }
#else
    @PostMapping("${action.path}")
    public AjaxResult ${action.code}(@RequestBody Map<String, Object> params)
    {
        return toAjax(${className}Service.${action.code}(params));
    }
#end

#end
#end
}
', 'admin', sysdate());

-- Admin frontend template files
INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES
(910201, 9102, 'package.json.vm', NULL, 'package.json.vm',
'{
  "name": "${projectFileName}-admin",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "vue-cli-service serve",
    "build": "vue-cli-service build"
  },
  "dependencies": {
    "element-ui": "2.15.14",
    "vue": "2.6.14",
    "vue-router": "3.6.5",
    "vuex": "3.6.2"
  }
}
', 'admin', sysdate()),
(910202, 9102, 'api.js.vm', NULL, 'api.js.vm',
'import request from "@/utils/request"

const baseUrl = "/${moduleName}/${businessName}"

export function list${ClassName}(query) {
  return request({
    url: baseUrl + "/list",
    method: "get",
    params: query
  })
}

export function get${ClassName}(${pkColumn.javaField}) {
  return request({
    url: baseUrl + "/" + ${pkColumn.javaField},
    method: "get"
  })
}

export function add${ClassName}(data) {
  return request({
    url: baseUrl,
    method: "post",
    data
  })
}

export function update${ClassName}(data) {
  return request({
    url: baseUrl,
    method: "put",
    data
  })
}

export function del${ClassName}(${pkColumn.javaField}) {
  return request({
    url: baseUrl + "/" + ${pkColumn.javaField},
    method: "delete"
  })
}

#if($hasTableBusinessActions)
#foreach($action in $tableBusinessActions)
export function ${action.code}(data) {
  return request({
    url: baseUrl + "${action.path}",
    method: "${action.method.toLowerCase()}",
#if($action.method == "GET")
    params: data
#else
    data
#end
  })
}

#end
#end
', 'admin', sysdate()),
(910203, 9102, 'index.vue.vm', NULL, 'index.vue.vm',
'<template>
  <div class="app-container">
    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="88px">
#foreach($column in $columns)
#if($column.isQuery == "1")
      <el-form-item label="${column.columnComment}" prop="${column.javaField}">
        <el-input
          v-model="queryParams.${column.javaField}"
          placeholder="请输入${column.columnComment}"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
#end
#end
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>

    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
      </el-col>
    </el-row>

    <el-table v-loading="loading" :data="rows" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" align="center" />
#foreach($column in $columns)
#if($column.isList == "1")
      <el-table-column label="${column.columnComment}" align="center" prop="${column.javaField}" />
#end
#end
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="260">
        <template slot-scope="scope">
          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
#if($hasTableBusinessActions)
          <el-dropdown size="mini" @command="command => handleBusinessAction(scope.row, command)">
            <el-button type="text" size="mini">业务<i class="el-icon-arrow-down el-icon--right"></i></el-button>
            <el-dropdown-menu slot="dropdown">
#foreach($action in $tableBusinessActions)
              <el-dropdown-item command="${action.code}">${action.name}</el-dropdown-item>
#end
            </el-dropdown-menu>
          </el-dropdown>
#end
        </template>
      </el-table-column>
    </el-table>

    <pagination
      v-show="total > 0"
      :total="total"
      :page.sync="queryParams.pageNum"
      :limit.sync="queryParams.pageSize"
      @pagination="getList"
    />

    <el-dialog :title="title" :visible.sync="open" width="560px" append-to-body>
      <el-form ref="form" :model="form" label-width="100px">
#foreach($column in $columns)
#if($column.isEdit == "1" && $column.isPk != "1")
        <el-form-item label="${column.columnComment}" prop="${column.javaField}">
          <el-input v-model="form.${column.javaField}" placeholder="请输入${column.columnComment}" />
        </el-form-item>
#end
#end
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitForm">确 定</el-button>
        <el-button @click="cancel">取 消</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import * as ${className}Api from "@/api/${moduleName}/${businessName}"

export default {
  name: "${ClassName}",
  data() {
    return {
      loading: false,
      rows: [],
      ids: [],
      total: 0,
      title: "",
      open: false,
      queryParams: {
        pageNum: 1,
        pageSize: 10
      },
      form: {},
      businessActions: [
#if($hasTableBusinessActions)
#foreach($action in $tableBusinessActions)
        { code: "${action.code}", name: "${action.name}" }#if($foreach.hasNext),#end
#end
#end
      ]
    }
  },
  created() {
    this.getList()
  },
  methods: {
    getList() {
      this.loading = true
      ${className}Api.list${ClassName}(this.queryParams).then(response => {
        this.rows = response.rows || response.data || []
        this.total = response.total || this.rows.length
        this.loading = false
      })
    },
    handleQuery() {
      this.queryParams.pageNum = 1
      this.getList()
    },
    resetQuery() {
      this.resetForm("queryForm")
      this.handleQuery()
    },
    handleSelectionChange(selection) {
      this.ids = selection.map(item => item.${pkColumn.javaField})
    },
    handleAdd() {
      this.form = {}
      this.open = true
      this.title = "新增${functionName}"
    },
    handleUpdate(row) {
      const id = row.${pkColumn.javaField}
      ${className}Api.get${ClassName}(id).then(response => {
        this.form = response.data || {}
        this.open = true
        this.title = "修改${functionName}"
      })
    },
    submitForm() {
      const request = this.form.${pkColumn.javaField} ? ${className}Api.update${ClassName}(this.form) : ${className}Api.add${ClassName}(this.form)
      request.then(() => {
        this.#[[$modal]]#.msgSuccess("保存成功")
        this.open = false
        this.getList()
      })
    },
    handleDelete(row) {
      const id = row.${pkColumn.javaField}
      ${className}Api.del${ClassName}(id).then(() => {
        this.#[[$modal]]#.msgSuccess("删除成功")
        this.getList()
      })
    },
    handleBusinessAction(row, actionCode) {
      const action = this.businessActions.find(item => item.code === actionCode)
      ${className}Api[actionCode]({ ...row }).then(() => {
        this.#[[$modal]]#.msgSuccess((action ? action.name : "业务动作") + "成功")
        this.getList()
      })
    },
    cancel() {
      this.open = false
    }
  }
}
</script>
', 'admin', sysdate());

INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES
(910204, 9102, 'adminMenus.js.vm', NULL, 'adminMenus.js.vm',
'export const appRoles = [
#foreach($role in $appRoles)
  { code: "${role.code}", name: "${role.name}", description: "${role.description}" }#if($foreach.hasNext),#end
#end
]

export const adminMenus = [
#foreach($menu in $adminMenus)
  {
    code: "${menu.code}",
    name: "${menu.name}",
    path: "${menu.path}",
    requiresLogin: ${menu.requiresLogin},
    visibleRoles: [
#foreach($role in $menu.visibleRoles)
      "${role}"#if($foreach.hasNext),#end
#end
    ],
    dataScope: "${menu.dataScope}"
  }#if($foreach.hasNext),#end
#end
]

export function requiresLogin(menu) {
  return Boolean(menu && menu.requiresLogin)
}

export function canShowMenu(menu, currentUser = {}) {
  if (!menu) {
    return false
  }
  if (menu.requiresLogin && !currentUser.id && !currentUser.userId) {
    return false
  }
  const userRoles = Array.isArray(currentUser.roles) ? currentUser.roles : []
  if (!Array.isArray(menu.visibleRoles) || menu.visibleRoles.length === 0) {
    return true
  }
  return menu.visibleRoles.some(role => userRoles.includes(role))
}
', 'admin', sysdate());

INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES
(910205, 9102, 'adminPermission.js.vm', NULL, 'adminPermission.js.vm',
'import { adminMenus, canShowMenu, requiresLogin } from "./config/adminMenus"

function defaultCurrentUser() {
  try {
    return JSON.parse(localStorage.getItem("currentUser") || "{}")
  } catch (error) {
    return {}
  }
}

function isLoggedIn(user) {
  return Boolean(user && (user.id || user.userId))
}

export function installAdminGuard(router, getCurrentUser = defaultCurrentUser) {
  router.beforeEach((to, from, next) => {
    const menu = adminMenus.find(item => item.path === to.path)
    const user = getCurrentUser() || {}
    if (menu && !canShowMenu(menu, user)) {
      next(false)
      return
    }
    if (menu && requiresLogin(menu) && !isLoggedIn(user)) {
      next("/login")
      return
    }
    next()
  })
}
', 'admin', sysdate());

-- Portal frontend template files
INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES
(910301, 9103, 'package.json.vm', NULL, 'package.json.vm',
'{
  "name": "${projectFileName}-portal",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite --host 0.0.0.0",
    "build": "vite build"
  },
  "dependencies": {
    "@vitejs/plugin-vue": "5.0.5",
    "axios": "1.7.2",
    "element-plus": "2.7.6",
    "vite": "5.3.1",
    "vue": "3.4.29",
    "vue-router": "4.4.0"
  },
  "devDependencies": {}
}
', 'admin', sysdate()),
(910302, 9103, 'index.html.vm', NULL, 'index.html.vm',
'<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>${projectName}</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>
', 'admin', sysdate()),
(910303, 9103, 'main.js.vm', NULL, 'main.js.vm',
'import { createApp } from "vue"
import ElementPlus from "element-plus"
import "element-plus/dist/index.css"
import App from "./App.vue"
import router from "./router"

createApp(App).use(router).use(ElementPlus).mount("#app")
', 'admin', sysdate()),
(910304, 9103, 'App.vue.vm', NULL, 'App.vue.vm',
'<template>
  <el-container class="shell">
    <el-header class="topbar">
      <strong>${projectName}</strong>
      <el-menu mode="horizontal" router :default-active="$route.path">
        <el-menu-item index="/">工作台</el-menu-item>
        <el-menu-item v-for="menu in visibleMenus" :key="menu.code" :index="menu.path">
          {{ menu.name }}
        </el-menu-item>
      </el-menu>
    </el-header>
    <el-main>
      <router-view />
    </el-main>
  </el-container>
</template>

<script setup>
import { computed, ref } from "vue"
import { canShowMenu, frontendMenus } from "./config/frontendMenus"

function loadCurrentUser() {
  try {
    return JSON.parse(localStorage.getItem("currentUser") || "{}")
  } catch (error) {
    return {}
  }
}

const currentUser = ref(loadCurrentUser())
const visibleMenus = computed(() => frontendMenus.filter(menu => canShowMenu(menu, currentUser.value)))
</script>

<style scoped>
.shell {
  min-height: 100vh;
  background: #f6f8fb;
}
.topbar {
  display: flex;
  align-items: center;
  gap: 24px;
  background: #ffffff;
  border-bottom: 1px solid #e5e7eb;
}
</style>
', 'admin', sysdate()),
(910305, 9103, 'router.js.vm', NULL, 'router.js.vm',
'import { createRouter, createWebHistory } from "vue-router"
import { canShowMenu, frontendMenus, requiresLogin } from "../config/frontendMenus"
#foreach($table in $tables)
import ${table.className}Index from "../views/${table.businessName}/index.vue"
import ${table.className}Detail from "../views/${table.businessName}/detail.vue"
#end

const routes = [
#if($hasTables)
#set($firstTable = $tables.get(0))
  {
    path: "/",
    redirect: "/${firstTable.businessName}"
  },
#else
  {
    path: "/",
    component: { template: "<main />" }
  }
#end
#foreach($table in $tables)
  {
    path: "/${table.businessName}",
    component: ${table.className}Index
  },
  {
    path: "/${table.businessName}/:id",
    component: ${table.className}Detail
  }#if($foreach.hasNext),#end
#end
]

function currentUser() {
  try {
    return JSON.parse(localStorage.getItem("currentUser") || "{}")
  } catch (error) {
    return {}
  }
}

function isLoggedIn(user) {
  return Boolean(user && (user.id || user.userId))
}

const router = createRouter({
  history: createWebHistory(),
  routes
})

router.beforeEach((to, from, next) => {
  const menu = frontendMenus.find(item => item.path === to.path)
  const user = currentUser()
  if (menu && !canShowMenu(menu, user)) {
    next(false)
    return
  }
  if (menu && requiresLogin(menu) && !isLoggedIn(user)) {
    next("/login")
    return
  }
  next()
})

export default router
', 'admin', sysdate()),
(910306, 9103, 'request.js.vm', NULL, 'request.js.vm',
'import axios from "axios"
import { ElMessage } from "element-plus"

const request = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_API || "",
  timeout: 15000
})

request.interceptors.response.use(
  response => response.data,
  error => {
    ElMessage.error(error.response?.data?.msg || error.message || "请求失败")
    return Promise.reject(error)
  }
)

export default request
', 'admin', sysdate()),
(910307, 9103, 'api.js.vm', NULL, 'api.js.vm',
'import request from "./request"

const baseUrl = "/${moduleName}/${businessName}"

export function list${ClassName}(query) {
  return request({
    url: baseUrl + "/list",
    method: "get",
    params: query
  })
}

export function get${ClassName}(${pkColumn.javaField}) {
  return request({
    url: baseUrl + "/" + ${pkColumn.javaField},
    method: "get"
  })
}

#if($hasTableBusinessActions)
#foreach($action in $tableBusinessActions)
export function ${action.code}(data) {
  return request({
    url: baseUrl + "${action.path}",
    method: "${action.method.toLowerCase()}",
#if($action.method == "GET")
    params: data
#else
    data
#end
  })
}

#end
#end
', 'admin', sysdate()),
(910308, 9103, 'index.vue.vm', NULL, 'index.vue.vm',
'<template>
  <section class="page">
    <div class="toolbar">
      <el-input v-model="keyword" placeholder="搜索${functionName}" clearable @keyup.enter="loadData" />
      <el-button type="primary" @click="loadData">搜索</el-button>
    </div>

    <el-table v-loading="loading" :data="rows" @row-click="openDetail">
#foreach($column in $columns)
#if($column.isList == "1")
      <el-table-column prop="${column.javaField}" label="${column.columnComment}" />
#end
#end
      <el-table-column label="业务" width="220">
        <template #default="{ row }">
#if($hasTableBusinessActions)
#foreach($action in $tableBusinessActions)
          <el-button size="small" @click.stop="runAction(row, ${foreach.index})">${action.name}</el-button>
#end
#else
          <span class="muted">无业务动作</span>
#end
        </template>
      </el-table-column>
    </el-table>
  </section>
</template>

<script setup>
import { onMounted, ref } from "vue"
import { useRouter } from "vue-router"
import { ElMessage } from "element-plus"
import * as api from "../../api/${businessName}"

const router = useRouter()
const loading = ref(false)
const rows = ref([])
const keyword = ref("")
const actions = [
#if($hasTableBusinessActions)
#foreach($action in $tableBusinessActions)
  { code: "${action.code}", name: "${action.name}" }#if($foreach.hasNext),#end
#end
#end
]

function loadData() {
  loading.value = true
  api.list${ClassName}({ keyword: keyword.value }).then(res => {
    rows.value = res.rows || res.data || []
  }).finally(() => {
    loading.value = false
  })
}

function openDetail(row) {
  router.push("/${businessName}/" + row.${pkColumn.javaField})
}

function runAction(row, index) {
  const action = actions[index]
  if (!action || !api[action.code]) {
    return
  }
  api[action.code]({ ...row }).then(() => {
    ElMessage.success(action.name + "成功")
    loadData()
  })
}

onMounted(loadData)
</script>

<style scoped>
.page {
  max-width: 1180px;
  margin: 0 auto;
}
.toolbar {
  display: flex;
  gap: 12px;
  margin-bottom: 16px;
}
.muted {
  color: #909399;
}
</style>
', 'admin', sysdate()),
(910309, 9103, 'detail.vue.vm', NULL, 'detail.vue.vm',
'<template>
  <section class="detail">
    <el-page-header @back="router.back()" content="${functionName}详情" />
    <el-descriptions :column="2" border>
#foreach($column in $columns)
      <el-descriptions-item label="${column.columnComment}">{{ detail.${column.javaField} }}</el-descriptions-item>
#end
    </el-descriptions>
    <div class="actions">
#if($hasTableBusinessActions)
#foreach($action in $tableBusinessActions)
      <el-button type="primary" @click="runAction(${foreach.index})">${action.name}</el-button>
#end
#end
    </div>
  </section>
</template>

<script setup>
import { onMounted, ref } from "vue"
import { useRoute, useRouter } from "vue-router"
import { ElMessage } from "element-plus"
import * as api from "../../api/${businessName}"

const route = useRoute()
const router = useRouter()
const detail = ref({})
const actions = [
#if($hasTableBusinessActions)
#foreach($action in $tableBusinessActions)
  { code: "${action.code}", name: "${action.name}" }#if($foreach.hasNext),#end
#end
#end
]

function loadDetail() {
  api.get${ClassName}(route.params.id).then(res => {
    detail.value = res.data || res
  })
}

function runAction(index) {
  const action = actions[index]
  if (!action || !api[action.code]) {
    return
  }
  api[action.code]({ ...detail.value }).then(() => {
    ElMessage.success(action.name + "成功")
    loadDetail()
  })
}

onMounted(loadDetail)
</script>

<style scoped>
.detail {
  max-width: 960px;
  margin: 0 auto;
}
.actions {
  margin-top: 16px;
  display: flex;
  gap: 12px;
}
</style>
', 'admin', sysdate());

INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES
(910310, 9103, 'frontendMenus.js.vm', NULL, 'frontendMenus.js.vm',
'export const appRoles = [
#foreach($role in $appRoles)
  { code: "${role.code}", name: "${role.name}", description: "${role.description}" }#if($foreach.hasNext),#end
#end
]

export const frontendMenus = [
#foreach($menu in $frontendMenus)
  {
    code: "${menu.code}",
    name: "${menu.name}",
    path: "${menu.path}",
    requiresLogin: ${menu.requiresLogin},
    visibleRoles: [
#foreach($role in $menu.visibleRoles)
      "${role}"#if($foreach.hasNext),#end
#end
    ],
    dataScope: "${menu.dataScope}"
  }#if($foreach.hasNext),#end
#end
]

export function requiresLogin(menu) {
  return Boolean(menu && menu.requiresLogin)
}

export function canShowMenu(menu, currentUser = {}) {
  if (!menu) {
    return false
  }
  if (menu.requiresLogin && !currentUser.id && !currentUser.userId) {
    return false
  }
  const userRoles = Array.isArray(currentUser.roles) ? currentUser.roles : []
  if (!Array.isArray(menu.visibleRoles) || menu.visibleRoles.length === 0) {
    return true
  }
  return menu.visibleRoles.some(role => userRoles.includes(role))
}
', 'admin', sysdate());

-- Backend project structure
INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES
(910100, 0, '{projectName}-backend', 'folder', NULL, 9101, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(910101, 910100, 'pom.xml', 'file', NULL, 9101, -1, 'pom.xml.vm', 1, '0', 'admin', sysdate(), '', NULL),
(910102, 910100, 'README.md', 'file', NULL, 9101, -1, 'README.md.vm', 2, '0', 'admin', sysdate(), '', NULL),
(910103, 910100, 'src', 'folder', NULL, 9101, NULL, NULL, 3, '0', 'admin', sysdate(), '', NULL),
(910104, 910103, 'main', 'folder', NULL, 9101, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(910105, 910104, 'java', 'folder', NULL, 9101, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(910106, 910105, '{packageName}', 'folder', NULL, 9101, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(910107, 910106, 'domain', 'folder', NULL, 9101, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(910108, 910106, 'mapper', 'folder', NULL, 9101, NULL, NULL, 2, '0', 'admin', sysdate(), '', NULL),
(910109, 910106, 'service', 'folder', NULL, 9101, NULL, NULL, 3, '0', 'admin', sysdate(), '', NULL),
(910110, 910109, 'impl', 'folder', NULL, 9101, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(910111, 910106, 'controller', 'folder', NULL, 9101, NULL, NULL, 4, '0', 'admin', sysdate(), '', NULL),
(910112, 910104, 'resources', 'folder', NULL, 9101, NULL, NULL, 2, '0', 'admin', sysdate(), '', NULL),
(910113, 910112, 'mapper', 'folder', NULL, 9101, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(910114, 910113, '{moduleName}', 'folder', NULL, 9101, 0, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(910115, 910112, 'application.yml', 'file', NULL, 9101, -1, 'application.yml.vm', 2, '0', 'admin', sysdate(), '', NULL),
(910116, 910107, '{ClassName}.java', 'file', NULL, 9101, 0, 'domain.java.vm', 1, '0', 'admin', sysdate(), '', NULL),
(910117, 910108, '{ClassName}Mapper.java', 'file', NULL, 9101, 0, 'mapper.java.vm', 1, '0', 'admin', sysdate(), '', NULL),
(910118, 910109, 'I{ClassName}Service.java', 'file', NULL, 9101, 0, 'service.java.vm', 1, '0', 'admin', sysdate(), '', NULL),
(910119, 910110, '{ClassName}ServiceImpl.java', 'file', NULL, 9101, 0, 'serviceImpl.java.vm', 1, '0', 'admin', sysdate(), '', NULL),
(910120, 910111, '{ClassName}Controller.java', 'file', NULL, 9101, 0, 'controller.java.vm', 1, '0', 'admin', sysdate(), '', NULL),
(910121, 910114, '{ClassName}Mapper.xml', 'file', NULL, 9101, 0, 'mapper.xml.vm', 1, '0', 'admin', sysdate(), '', NULL);

-- Admin frontend project structure
INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES
(910200, 0, '{projectName}-admin', 'folder', NULL, 9102, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(910201, 910200, 'package.json', 'file', NULL, 9102, -1, 'package.json.vm', 1, '0', 'admin', sysdate(), '', NULL),
(910202, 910200, 'src', 'folder', NULL, 9102, NULL, NULL, 2, '0', 'admin', sysdate(), '', NULL),
(910203, 910202, 'api', 'folder', NULL, 9102, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(910204, 910203, '{moduleName}', 'folder', NULL, 9102, 0, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(910205, 910204, '{businessName}.js', 'file', NULL, 9102, 0, 'api.js.vm', 1, '0', 'admin', sysdate(), '', NULL),
(910206, 910202, 'views', 'folder', NULL, 9102, NULL, NULL, 2, '0', 'admin', sysdate(), '', NULL),
(910207, 910206, '{moduleName}', 'folder', NULL, 9102, 0, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(910208, 910207, '{businessName}', 'folder', NULL, 9102, 0, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(910209, 910208, 'index.vue', 'file', NULL, 9102, 0, 'index.vue.vm', 1, '0', 'admin', sysdate(), '', NULL);

INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES
(910210, 910202, 'config', 'folder', NULL, 9102, NULL, NULL, 3, '0', 'admin', sysdate(), '', NULL),
(910211, 910210, 'adminMenus.js', 'file', NULL, 9102, -1, 'adminMenus.js.vm', 1, '0', 'admin', sysdate(), '', NULL);

INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES
(910212, 910202, 'permission.js', 'file', NULL, 9102, -1, 'adminPermission.js.vm', 4, '0', 'admin', sysdate(), '', NULL);

-- Portal frontend project structure
INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES
(910300, 0, '{projectName}-portal', 'folder', NULL, 9103, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(910301, 910300, 'package.json', 'file', NULL, 9103, -1, 'package.json.vm', 1, '0', 'admin', sysdate(), '', NULL),
(910302, 910300, 'index.html', 'file', NULL, 9103, -1, 'index.html.vm', 2, '0', 'admin', sysdate(), '', NULL),
(910303, 910300, 'src', 'folder', NULL, 9103, NULL, NULL, 3, '0', 'admin', sysdate(), '', NULL),
(910304, 910303, 'main.js', 'file', NULL, 9103, -1, 'main.js.vm', 1, '0', 'admin', sysdate(), '', NULL),
(910305, 910303, 'App.vue', 'file', NULL, 9103, -1, 'App.vue.vm', 2, '0', 'admin', sysdate(), '', NULL),
(910306, 910303, 'router', 'folder', NULL, 9103, NULL, NULL, 3, '0', 'admin', sysdate(), '', NULL),
(910307, 910306, 'index.js', 'file', NULL, 9103, -1, 'router.js.vm', 1, '0', 'admin', sysdate(), '', NULL),
(910308, 910303, 'api', 'folder', NULL, 9103, NULL, NULL, 4, '0', 'admin', sysdate(), '', NULL),
(910309, 910308, 'request.js', 'file', NULL, 9103, -1, 'request.js.vm', 1, '0', 'admin', sysdate(), '', NULL),
(910310, 910308, '{businessName}.js', 'file', NULL, 9103, 0, 'api.js.vm', 2, '0', 'admin', sysdate(), '', NULL),
(910311, 910303, 'views', 'folder', NULL, 9103, NULL, NULL, 5, '0', 'admin', sysdate(), '', NULL),
(910312, 910311, '{businessName}', 'folder', NULL, 9103, 0, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(910313, 910312, 'index.vue', 'file', NULL, 9103, 0, 'index.vue.vm', 1, '0', 'admin', sysdate(), '', NULL),
(910314, 910312, 'detail.vue', 'file', NULL, 9103, 0, 'detail.vue.vm', 2, '0', 'admin', sysdate(), '', NULL);

INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES
(910315, 910303, 'config', 'folder', NULL, 9103, NULL, NULL, 6, '0', 'admin', sysdate(), '', NULL),
(910316, 910315, 'frontendMenus.js', 'file', NULL, 9103, -1, 'frontendMenus.js.vm', 1, '0', 'admin', sysdate(), '', NULL);

SET FOREIGN_KEY_CHECKS = 1;
