chore: 从 git 中移除 backend/target 目录

- 使用 git rm --cached 从版本控制中删除 target 目录
- .gitignore 已配置忽略 target 和 node_modules
- 本地文件保留,仅从 git 索引中删除

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
wanglongjie 2026-01-26 16:12:04 +08:00
parent 934b8aad40
commit ea8f8390da
99 changed files with 3551 additions and 724 deletions

View File

@ -1,81 +0,0 @@
server:
port: 8080
servlet:
context-path: /api
spring:
application:
name: by-crm
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.3.80:3306/by_crm?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: "Boyun@123"
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
filters: stat,wall
web-stat-filter:
enabled: true
url-pattern: /*
stat-view-servlet:
enabled: true
url-pattern: /druid/*
reset-enable: false
login-username: admin
login-password: admin123
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
default-property-inclusion: non_null
mvc:
pathmatch:
matching-strategy: ant_path_matcher
# MyBatis 配置
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.bycrm.entity
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# JWT 配置
jwt:
# HS512 算法要求密钥至少 64 字节512 位)
# 生产环境请使用环境变量或密钥管理系统存储此密钥
secret: by-crm-jwt-secret-key-2024-hs512-requires-at-least-64-bytes-for-secure-signing-please-change-in-production-environment
expiration: 86400000 # 24小时单位毫秒
# CRM 业务配置
crm:
report:
ttl-days: 90 # 保护期天数
allow-overlap: false # 是否允许重叠报备(生产环境必须为 false
# Swagger 配置
springfox:
documentation:
swagger-ui:
enabled: true
enabled: true
# 日志配置
logging:
level:
com.bycrm.mapper: debug
org.springframework.web: info
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'

View File

@ -1,119 +0,0 @@
<?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.bycrm.mapper.CustomerMapper">
<resultMap id="BaseResultMap" type="com.bycrm.entity.Customer">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="phone" property="phone"/>
<result column="address" property="address"/>
<result column="industry" property="industry"/>
<result column="status" property="status"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
</resultMap>
<resultMap id="CustomerVOResultMap" type="com.bycrm.vo.CustomerVO" extends="BaseResultMap">
<result column="status_desc" property="statusDesc"/>
<result column="current_dealer_id" property="currentDealerId"/>
<result column="current_dealer_name" property="currentDealerName"/>
<result column="protect_end_date" property="protectEndDate"/>
</resultMap>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM crm_customer WHERE id = #{id}
</select>
<select id="selectByNameLike" resultMap="BaseResultMap">
SELECT * FROM crm_customer
WHERE name LIKE CONCAT('%', #{name}, '%')
LIMIT 10
</select>
<select id="selectPage" resultMap="BaseResultMap">
SELECT * FROM crm_customer
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="industry != null and industry != ''">
AND industry = #{industry}
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
ORDER BY created_at DESC
LIMIT #{query.size} OFFSET #{query.offset}
</select>
<select id="countPage" resultType="java.lang.Long">
SELECT COUNT(*) FROM crm_customer
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="industry != null and industry != ''">
AND industry = #{industry}
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
</select>
<insert id="insert" parameterType="com.bycrm.entity.Customer" useGeneratedKeys="true" keyProperty="id">
INSERT INTO crm_customer (name, phone, address, industry, status)
VALUES (#{name}, #{phone}, #{address}, #{industry}, #{status})
</insert>
<update id="update" parameterType="com.bycrm.entity.Customer">
UPDATE crm_customer
<set>
<if test="name != null and name != ''">name = #{name},</if>
<if test="phone != null">phone = #{phone},</if>
<if test="address != null">address = #{address},</if>
<if test="industry != null">industry = #{industry},</if>
<if test="status != null">status = #{status},</if>
</set>
WHERE id = #{id}
</update>
<delete id="deleteById">
DELETE FROM crm_customer WHERE id = #{id}
</delete>
<select id="selectPageWithDealer" resultMap="CustomerVOResultMap">
SELECT
c.*,
CASE c.status
WHEN 0 THEN '可报备'
WHEN 1 THEN '保护中'
WHEN 2 THEN '已失效'
ELSE '未知'
END AS status_desc,
r.dealer_id AS current_dealer_id,
d.name AS current_dealer_name,
r.protect_end_date
FROM crm_customer c
LEFT JOIN crm_report r ON c.id = r.customer_id
AND r.status = 1
AND r.protect_end_date > NOW()
LEFT JOIN crm_dealer d ON r.dealer_id = d.id
<where>
<if test="name != null and name != ''">
AND c.name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="industry != null and industry != ''">
AND c.industry = #{industry}
</if>
<if test="status != null">
AND c.status = #{status}
</if>
</where>
ORDER BY c.created_at DESC
LIMIT #{query.size} OFFSET #{query.offset}
</select>
</mapper>

View File

@ -1,71 +0,0 @@
<?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.bycrm.mapper.DealerMapper">
<resultMap id="BaseResultMap" type="com.bycrm.entity.Dealer">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="code" property="code"/>
<result column="contact_person" property="contactPerson"/>
<result column="contact_phone" property="contactPhone"/>
<result column="email" property="email"/>
<result column="status" property="status"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
<result column="user_id" property="userId"/>
</resultMap>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM crm_dealer WHERE id = #{id}
</select>
<select id="selectByCode" resultMap="BaseResultMap">
SELECT * FROM crm_dealer WHERE code = #{code}
</select>
<select id="selectList" resultMap="BaseResultMap">
SELECT d.*, u.id as user_id
FROM crm_dealer d
LEFT JOIN crm_user u ON u.dealer_id = d.id
<where>
<if test="name != null and name != ''">
AND d.name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="code != null and code != ''">
AND d.code LIKE CONCAT('%', #{code}, '%')
</if>
<if test="status != null">
AND d.status = #{status}
</if>
</where>
ORDER BY d.created_at DESC
</select>
<insert id="insert" parameterType="com.bycrm.entity.Dealer" useGeneratedKeys="true" keyProperty="id">
INSERT INTO crm_dealer (name, code, contact_person, contact_phone, email, status)
VALUES (#{name}, #{code}, #{contactPerson}, #{contactPhone}, #{email}, #{status})
</insert>
<update id="update" parameterType="com.bycrm.entity.Dealer">
UPDATE crm_dealer
<set>
<if test="name != null and name != ''">name = #{name},</if>
<if test="code != null and code != ''">code = #{code},</if>
<if test="contactPerson != null and contactPerson != ''">contact_person = #{contactPerson},</if>
<if test="contactPhone != null and contactPhone != ''">contact_phone = #{contactPhone},</if>
<if test="email != null">email = #{email},</if>
<if test="status != null">status = #{status},</if>
</set>
WHERE id = #{id}
</update>
<delete id="deleteById">
DELETE FROM crm_dealer WHERE id = #{id}
</delete>
<select id="count" resultType="java.lang.Long">
SELECT COUNT(*) FROM crm_dealer
</select>
</mapper>

View File

@ -1,141 +0,0 @@
<?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.bycrm.mapper.ReportMapper">
<resultMap id="BaseResultMap" type="com.bycrm.entity.Report">
<id column="id" property="id"/>
<result column="dealer_id" property="dealerId"/>
<result column="customer_id" property="customerId"/>
<result column="description" property="description"/>
<result column="status" property="status"/>
<result column="reject_reason" property="rejectReason"/>
<result column="protect_start_date" property="protectStartDate"/>
<result column="protect_end_date" property="protectEndDate"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
<result column="dealer_name" property="dealerName"/>
<result column="customer_name" property="customerName"/>
<result column="customer_phone" property="customerPhone"/>
</resultMap>
<select id="selectById" resultMap="BaseResultMap">
SELECT r.*,
d.name AS dealer_name,
c.name AS customer_name,
c.phone AS customer_phone
FROM crm_report r
LEFT JOIN crm_dealer d ON r.dealer_id = d.id
LEFT JOIN crm_customer c ON r.customer_id = c.id
WHERE r.id = #{id}
</select>
<select id="selectValidByCustomerId" resultMap="BaseResultMap">
SELECT * FROM crm_report
WHERE customer_id = #{customerId}
AND status IN (0, 1)
LIMIT 1
</select>
<select id="selectPage" resultMap="BaseResultMap">
SELECT r.*,
d.name AS dealer_name,
c.name AS customer_name,
c.phone AS customer_phone
FROM crm_report r
LEFT JOIN crm_dealer d ON r.dealer_id = d.id
LEFT JOIN crm_customer c ON r.customer_id = c.id
<where>
<if test="dealerId != null">
AND r.dealer_id = #{dealerId}
</if>
<if test="dealerName != null and dealerName != ''">
AND d.name LIKE CONCAT('%', #{dealerName}, '%')
</if>
<if test="customerName != null and customerName != ''">
AND c.name LIKE CONCAT('%', #{customerName}, '%')
</if>
<if test="status != null">
AND r.status = #{status}
</if>
</where>
ORDER BY r.created_at DESC
LIMIT #{query.size} OFFSET #{query.offset}
</select>
<select id="countPage" resultType="java.lang.Long">
SELECT COUNT(*)
FROM crm_report r
LEFT JOIN crm_dealer d ON r.dealer_id = d.id
LEFT JOIN crm_customer c ON r.customer_id = c.id
<where>
<if test="dealerId != null">
AND r.dealer_id = #{dealerId}
</if>
<if test="dealerName != null and dealerName != ''">
AND d.name LIKE CONCAT('%', #{dealerName}, '%')
</if>
<if test="customerName != null and customerName != ''">
AND c.name LIKE CONCAT('%', #{customerName}, '%')
</if>
<if test="status != null">
AND r.status = #{status}
</if>
</where>
</select>
<insert id="insert" parameterType="com.bycrm.entity.Report" useGeneratedKeys="true" keyProperty="id">
INSERT INTO crm_report (dealer_id, customer_id, description, status, protect_start_date, protect_end_date)
VALUES (#{dealerId}, #{customerId}, #{description}, #{status}, #{protectStartDate}, #{protectEndDate})
</insert>
<update id="update" parameterType="com.bycrm.entity.Report">
UPDATE crm_report
<set>
<if test="status != null">status = #{status},</if>
<if test="rejectReason != null">reject_reason = #{rejectReason},</if>
<if test="protectStartDate != null">protect_start_date = #{protectStartDate},</if>
<if test="protectEndDate != null">protect_end_date = #{protectEndDate},</if>
</set>
WHERE id = #{id}
</update>
<delete id="deleteById">
DELETE FROM crm_report WHERE id = #{id}
</delete>
<select id="selectExpiringReports" resultMap="BaseResultMap">
SELECT * FROM crm_report
WHERE status = 1
AND protect_end_date &lt;= #{endDate}
</select>
<update id="batchUpdateExpired">
UPDATE crm_report
SET status = 3
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
<select id="countByDealerId" resultType="java.lang.Long">
SELECT COUNT(*)
FROM crm_report
<where>
<if test="dealerId != null">
AND dealer_id = #{dealerId}
</if>
</where>
</select>
<select id="countPendingByDealerId" resultType="java.lang.Long">
SELECT COUNT(*)
FROM crm_report
WHERE status = 0
<if test="dealerId != null">
AND dealer_id = #{dealerId}
</if>
</select>
</mapper>

View File

@ -1,59 +0,0 @@
<?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.bycrm.mapper.SchoolMapper">
<resultMap id="BaseResultMap" type="com.bycrm.entity.School">
<id column="id" property="id"/>
<result column="school_code" property="schoolCode"/>
<result column="school_name" property="schoolName"/>
<result column="location" property="location"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
</resultMap>
<insert id="insert" parameterType="com.bycrm.entity.School" useGeneratedKeys="true" keyProperty="id">
INSERT INTO crm_school (school_code, school_name, location, created_at, updated_at)
VALUES (#{schoolCode}, #{schoolName}, #{location}, #{createdAt}, #{updatedAt})
</insert>
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO crm_school (school_code, school_name, location, created_at, updated_at)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.schoolCode}, #{item.schoolName}, #{item.location}, #{item.createdAt}, #{item.updatedAt})
</foreach>
</insert>
<select id="searchByName" resultMap="BaseResultMap">
SELECT id, school_code, school_name, location, created_at, updated_at
FROM crm_school
WHERE school_name LIKE CONCAT('%', #{keyword}, '%')
ORDER BY school_name
LIMIT 50
</select>
<select id="findByCode" resultMap="BaseResultMap">
SELECT id, school_code, school_name, location, created_at, updated_at
FROM crm_school
WHERE school_code = #{schoolCode}
</select>
<select id="findAll" resultMap="BaseResultMap">
SELECT id, school_code, school_name, location, created_at, updated_at
FROM crm_school
ORDER BY school_name
</select>
<select id="findByPage" resultMap="BaseResultMap">
SELECT id, school_code, school_name, location, created_at, updated_at
FROM crm_school
ORDER BY school_name
LIMIT #{offset}, #{limit}
</select>
<select id="count" resultType="int">
SELECT COUNT(*) FROM crm_school
</select>
</mapper>

View File

@ -1,46 +0,0 @@
<?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.bycrm.mapper.SystemConfigMapper">
<resultMap id="BaseResultMap" type="com.bycrm.entity.SystemConfig">
<id column="id" property="id"/>
<result column="config_key" property="configKey"/>
<result column="config_value" property="configValue"/>
<result column="config_label" property="configLabel"/>
<result column="config_type" property="configType"/>
<result column="description" property="description"/>
<result column="is_editable" property="isEditable"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
</resultMap>
<select id="selectAll" resultMap="BaseResultMap">
SELECT * FROM crm_system_config ORDER BY id
</select>
<select id="selectByKey" resultMap="BaseResultMap">
SELECT * FROM crm_system_config WHERE config_key = #{configKey}
</select>
<select id="selectValueByKey" resultType="java.lang.String">
SELECT config_value FROM crm_system_config WHERE config_key = #{configKey}
</select>
<update id="update" parameterType="com.bycrm.entity.SystemConfig">
UPDATE crm_system_config
SET config_value = #{configValue},
updated_at = NOW()
WHERE config_key = #{configKey}
</update>
<update id="batchUpdate">
<foreach collection="configs" item="config" separator=";">
UPDATE crm_system_config
SET config_value = #{config.configValue},
updated_at = NOW()
WHERE config_key = #{config.configKey}
</foreach>
</update>
</mapper>

View File

@ -1,64 +0,0 @@
<?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.bycrm.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.bycrm.entity.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="real_name" property="realName"/>
<result column="dealer_id" property="dealerId"/>
<result column="role" property="role"/>
<result column="status" property="status"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
<result column="dealer_name" property="dealerName"/>
</resultMap>
<select id="selectByUsername" resultMap="BaseResultMap">
SELECT u.*,
d.name AS dealer_name
FROM crm_user u
LEFT JOIN crm_dealer d ON u.dealer_id = d.id
WHERE u.username = #{username}
</select>
<select id="selectById" resultMap="BaseResultMap">
SELECT u.*,
d.name AS dealer_name
FROM crm_user u
LEFT JOIN crm_dealer d ON u.dealer_id = d.id
WHERE u.id = #{id}
</select>
<select id="selectList" resultMap="BaseResultMap">
SELECT u.*,
d.name AS dealer_name
FROM crm_user u
LEFT JOIN crm_dealer d ON u.dealer_id = d.id
ORDER BY u.created_at DESC
</select>
<insert id="insert" parameterType="com.bycrm.entity.User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO crm_user (username, password, real_name, dealer_id, role, status)
VALUES (#{username}, #{password}, #{realName}, #{dealerId}, #{role}, #{status})
</insert>
<update id="update" parameterType="com.bycrm.entity.User">
UPDATE crm_user
<set>
<if test="password != null and password != ''">password = #{password},</if>
<if test="realName != null and realName != ''">real_name = #{realName},</if>
<if test="dealerId != null">dealer_id = #{dealerId},</if>
<if test="role != null">role = #{role},</if>
<if test="status != null">status = #{status},</if>
</set>
WHERE id = #{id}
</update>
<delete id="deleteById">
DELETE FROM crm_user WHERE id = #{id}
</delete>
</mapper>

View File

@ -1,46 +0,0 @@
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\DealerDTO.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\controller\DealerController.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\ReportAuditDTO.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\impl\ReportServiceImpl.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\LoginDTO.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\controller\AuthController.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\util\JwtUtil.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\PageQuery.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\common\Result.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\Dict.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\exception\BusinessException.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\WebMvcConfig.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\vo\UserInfoVO.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\mapper\DealerMapper.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\mapper\UserMapper.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\Report.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\common\Constants.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\common\PageResult.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\CorsConfig.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\controller\ReportController.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\Customer.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\UserService.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\SwaggerConfig.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\ByCrmApplication.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\ReportService.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\User.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\Dealer.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\DealerService.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\mapper\ReportMapper.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\impl\DealerServiceImpl.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\OperationLog.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\vo\CustomerVO.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\DictItem.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\exception\GlobalExceptionHandler.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\vo\ReportVO.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\CustomerService.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\MyBatisConfig.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\CustomerDTO.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\ReportDTO.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\mapper\CustomerMapper.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\controller\CustomerController.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\impl\CustomerServiceImpl.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\annotations\AuthRequired.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\AuthInterceptor.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\impl\UserServiceImpl.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\task\ReportExpireTask.java

View File

@ -1 +0,0 @@
com\bycrm\util\PasswordTest.class

View File

@ -1,47 +0,0 @@
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\DealerDTO.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\controller\DealerController.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\ReportAuditDTO.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\impl\ReportServiceImpl.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\LoginDTO.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\controller\AuthController.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\util\JwtUtil.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\PageQuery.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\common\Result.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\Dict.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\exception\BusinessException.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\WebMvcConfig.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\vo\UserInfoVO.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\mapper\DealerMapper.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\mapper\UserMapper.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\Report.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\common\Constants.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\common\PageResult.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\CorsConfig.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\controller\ReportController.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\util\PasswordTest.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\Customer.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\UserService.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\SwaggerConfig.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\ByCrmApplication.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\ReportService.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\User.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\Dealer.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\DealerService.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\mapper\ReportMapper.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\impl\DealerServiceImpl.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\OperationLog.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\vo\CustomerVO.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\entity\DictItem.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\exception\GlobalExceptionHandler.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\vo\ReportVO.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\CustomerService.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\MyBatisConfig.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\CustomerDTO.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\dto\ReportDTO.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\mapper\CustomerMapper.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\controller\CustomerController.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\impl\CustomerServiceImpl.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\annotations\AuthRequired.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\config\AuthInterceptor.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\service\impl\UserServiceImpl.java
E:\boyun-workspace\by-crm\backend\src\main\java\com\bycrm\task\ReportExpireTask.java

View File

@ -10,27 +10,29 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.2.0",
"@element-plus/icons-vue": "^2.3.0",
"axios": "^1.6.0",
"bcryptjs": "^3.0.3",
"dayjs": "^1.11.0",
"element-plus": "^2.5.0",
"pinia": "^2.1.0",
"pinia-plugin-persistedstate": "^3.2.0",
"axios": "^1.6.0",
"element-plus": "^2.5.0",
"@element-plus/icons-vue": "^2.3.0",
"dayjs": "^1.11.0"
"vue": "^3.4.0",
"vue-router": "^4.2.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"vite": "^5.0.0",
"vue-tsc": "^1.8.0",
"typescript": "^5.3.0",
"@types/bcryptjs": "^3.0.0",
"@types/node": "^20.10.0",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"@vitejs/plugin-vue": "^5.0.0",
"eslint": "^8.55.0",
"eslint-plugin-vue": "^9.19.0",
"@typescript-eslint/parser": "^6.15.0",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"prettier": "^3.1.0",
"sass": "^1.69.0"
"sass": "^1.69.0",
"typescript": "^5.3.0",
"vite": "^5.0.0",
"vue-tsc": "^1.8.0"
},
"engines": {
"node": ">=16.0.0",

2664
frontend/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -15,3 +15,13 @@ export const getUserInfo = () => {
export const logout = () => {
return http.post('/auth/logout')
}
// 修改密码
export const changePassword = (data: { oldPassword: string; newPassword: string }) => {
return http.post('/auth/change-password', data)
}
// 重置密码(管理员功能)
export const resetPassword = (data: { userId: number; newPassword: string }) => {
return http.post('/auth/reset-password', data)
}

View File

@ -0,0 +1,18 @@
import request from '@/utils/request'
export interface DashboardStatistics {
customerCount: number
reportCount: number
pendingCount: number
dealerCount: number
}
/**
*
*/
export function getStatistics() {
return request<DashboardStatistics>({
url: '/dashboard/statistics',
method: 'get'
})
}

View File

@ -0,0 +1,45 @@
import { http } from '@/utils/request'
// 学校接口类型定义
export interface School {
id?: number
schoolCode: string
schoolName: string
location?: string
createdAt?: string
updatedAt?: string
}
// 根据名称搜索学校(用于自动完成)
export const searchSchoolByName = (keyword: string) => {
return http.get<School[]>('/school/search', { params: { keyword } })
}
// 查询所有学校
export const getAllSchools = () => {
return http.get<School[]>('/school/list')
}
// 创建学校
export const createSchool = (data: Omit<School, 'id' | 'createdAt' | 'updatedAt'>) => {
return http.post<number>('/school', data)
}
// 批量导入学校
export const batchImportSchools = (data: School[]) => {
return http.post<number>('/school/batch', data)
}
// 从Excel文件导入学校数据
export const importSchoolsFromExcel = (file: File) => {
const formData = new FormData()
formData.append('file', file)
return http.post<number>('/school/import', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
// 从项目docs目录导入学校数据
export const importSchoolsFromDocs = () => {
return http.post<number>('/school/import-from-docs')
}

View File

@ -0,0 +1,44 @@
import request from '@/utils/request'
import type { SystemConfig } from '@/types'
/**
*
*/
export function getAllConfigs() {
return request<SystemConfig[]>({
url: '/system/config',
method: 'get'
})
}
/**
*
*/
export function getConfigValue(configKey: string) {
return request<string>({
url: `/system/config/value/${configKey}`,
method: 'get'
})
}
/**
*
*/
export function updateConfig(configKey: string, configValue: string) {
return request({
url: '/system/config',
method: 'put',
data: { configKey, configValue }
})
}
/**
*
*/
export function batchUpdateConfigs(configs: Record<string, string>) {
return request({
url: '/system/config/batch',
method: 'put',
data: configs
})
}

View File

@ -37,6 +37,12 @@ const routes: RouteRecordRaw[] = [
name: 'Dealer',
component: () => import('@/views/Dealer.vue'),
meta: { title: '经销商管理', requiresAdmin: true }
},
{
path: 'system-config',
name: 'SystemConfig',
component: () => import('@/views/SystemConfig.vue'),
meta: { title: '系统配置', requiresAdmin: true }
}
]
}

View File

@ -27,6 +27,9 @@ export interface Customer {
industry?: string
status: number
statusDesc: string
currentDealerId?: number
currentDealerName?: string
protectEndDate?: string
createdAt: string
updatedAt: string
}
@ -47,6 +50,7 @@ export interface Dealer {
contactPhone: string
email?: string
status: number
userId: number // 关联的用户ID用于重置密码
createdAt: string
updatedAt: string
}
@ -99,3 +103,16 @@ export interface DictItem {
label: string
value: string | number
}
// 系统配置相关类型
export interface SystemConfig {
id: number
configKey: string
configValue: string
configLabel: string
configType: 'string' | 'integer' | 'boolean'
description?: string
isEditable: number
createdAt: string
updatedAt: string
}

View File

@ -0,0 +1,61 @@
/**
*
* 使 Web Crypto API SHA-256
*/
/**
* ArrayBuffer
*/
function stringToArrayBuffer(str: string): ArrayBuffer {
const encoder = new TextEncoder()
return encoder.encode(str).buffer
}
/**
* ArrayBuffer
*/
function arrayBufferToHex(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer)
return Array.from(bytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('')
}
/**
* 使 SHA-256
* @param password
* @returns
*/
export async function hashPassword(password: string): Promise<string> {
if (!password) return ''
try {
// 使用 Web Crypto API 进行 SHA-256 哈希
const data = stringToArrayBuffer(password)
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
return arrayBufferToHex(hashBuffer)
} catch (error) {
console.error('密码哈希失败:', error)
throw new Error('密码加密失败')
}
}
/**
*
* @param password
* @param salt
* @returns
*/
export async function hashPasswordWithSalt(password: string, salt: string): Promise<string> {
if (!password) return ''
try {
const saltedPassword = password + salt
const data = stringToArrayBuffer(saltedPassword)
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
return arrayBufferToHex(hashBuffer)
} catch (error) {
console.error('密码哈希失败:', error)
throw new Error('密码加密失败')
}
}

View File

@ -0,0 +1,70 @@
import * as bcrypt from 'bcryptjs'
/**
*
* BCrypt
* 使 BCrypt.matches()
*/
/**
* BCrypt
* @param password
* @returns BCrypt
*/
export function hashPassword(password: string): string {
const salt = bcrypt.genSaltSync(10)
return bcrypt.hashSync(password, salt)
}
/**
* BCrypt
*
* @param password
* @param hash BCrypt
* @returns
*/
export function verifyPassword(password: string, hash: string): boolean {
return bcrypt.compareSync(password, hash)
}
/**
*
*
* 1. 使 BCrypt
* 2. MD5/SHA256
*
* 使 BCrypt
* - BCrypt
* - 使 BCrypt.matches()
* -
*/
/**
* HTTPS
*/
export function preparePasswordForLogin(password: string): string {
return password // 明文传输,后端 BCrypt 验证
}
/**
* 使
* BCrypt SHA-256
*/
export async function hashPasswordForLogin(password: string): Promise<string> {
// 使用 Web Crypto API 进行 SHA-256 哈希
const encoder = new TextEncoder()
const data = encoder.encode(password)
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
return hashHex
}
/**
*
* 使 BCrypt
*
*/
export function getLoginPassword(password: string): string {
return password
}

View File

@ -4,11 +4,16 @@
<template #header>
<div class="card-header">
<span>客户管理</span>
<div style="display: flex; gap: 10px; align-items: center">
<el-tag type="info" size="large">
当前保护期配置{{ protectDays }}
</el-tag>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增客户
</el-button>
</div>
</div>
</template>
<!-- 查询表单 -->
@ -60,6 +65,16 @@
<el-tag v-else type="info">已失效</el-tag>
</template>
</el-table-column>
<el-table-column prop="currentDealerName" label="当前经销商" width="150">
<template #default="{ row }">
{{ row.status === 1 ? (row.currentDealerName || '-') : '-' }}
</template>
</el-table-column>
<el-table-column prop="protectEndDate" label="保护期截止" width="180">
<template #default="{ row }">
{{ row.status === 1 && row.protectEndDate ? formatDate(row.protectEndDate) : '-' }}
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="180" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
@ -76,6 +91,8 @@
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
prev-text="上一页"
next-text="下一页"
@size-change="fetchData"
@current-change="fetchData"
style="margin-top: 20px; justify-content: flex-end"
@ -91,7 +108,21 @@
>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="客户名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入客户名称" />
<el-autocomplete
v-model="formData.name"
:fetch-suggestions="searchSchool"
placeholder="请输入客户名称"
:trigger-on-focus="false"
style="width: 100%"
@select="handleSchoolSelect"
>
<template #default="{ item }">
<div class="school-option">
<div class="school-name">{{ item.schoolName }}</div>
<div class="school-location">{{ item.location }}</div>
</div>
</template>
</el-autocomplete>
</el-form-item>
<el-form-item label="联系电话" prop="phone">
<el-input v-model="formData.phone" placeholder="请输入联系电话" />
@ -123,7 +154,12 @@
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { getCustomerPage, createCustomer, updateCustomer, deleteCustomer } from '@/api/customer'
import { getConfigValue } from '@/api/system'
import { searchSchoolByName } from '@/api/school'
import type { Customer, CustomerForm } from '@/types'
import type { School } from '@/api/school'
const protectDays = ref<number>(90)
const loading = ref(false)
const tableData = ref<Customer[]>([])
@ -131,6 +167,8 @@ const total = ref(0)
const dialogVisible = ref(false)
const dialogTitle = computed(() => (formData.id ? '编辑客户' : '新增客户'))
const formRef = ref<FormInstance>()
const schoolSearchLoading = ref(false)
const schoolOptions = ref<School[]>([])
const queryForm = reactive({
current: 1,
@ -151,6 +189,44 @@ const rules: FormRules = {
name: [{ required: true, message: '请输入客户名称', trigger: 'blur' }]
}
//
const formatDate = (dateStr: string | null | undefined) => {
if (!dateStr) return '-'
return dateStr.split(' ')[0] //
}
//
const searchSchool = async (queryString: string, cb: any) => {
if (!queryString || queryString.trim().length < 1) {
cb([])
return
}
schoolSearchLoading.value = true
try {
const schools = await searchSchoolByName(queryString)
// el-autocomplete
const suggestions = schools.map(school => ({
...school,
value: school.schoolName
}))
cb(suggestions)
} catch (error) {
console.error('搜索学校失败', error)
cb([])
} finally {
schoolSearchLoading.value = false
}
}
//
const handleSchoolSelect = (item: School) => {
//
if (item.location && !formData.address) {
formData.address = item.location
}
}
//
const getIndustryLabel = (industry: string) => {
const map: Record<string, string> = {
@ -208,7 +284,15 @@ const handleEdit = (row: Customer) => {
//
const handleDelete = async (row: Customer) => {
try {
await ElMessageBox.confirm('确定要删除该客户吗?', '提示', { type: 'warning' })
await ElMessageBox.confirm(
'确定要删除该客户吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
await deleteCustomer(row.id)
ElMessage.success('删除成功')
fetchData()
@ -251,7 +335,18 @@ const handleDialogClose = () => {
delete formData.id
}
//
const fetchProtectDays = async () => {
try {
const res = await getConfigValue('report.protect.days')
protectDays.value = parseInt(res) || 90
} catch (error) {
console.error('获取配置失败', error)
}
}
onMounted(() => {
fetchProtectDays()
fetchData()
})
</script>
@ -270,4 +365,20 @@ onMounted(() => {
.query-form {
margin-bottom: 20px;
}
.school-option {
display: flex;
flex-direction: column;
}
.school-name {
font-weight: 500;
color: #303133;
}
.school-location {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
</style>

View File

@ -65,8 +65,9 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
import * as statisticsApi from '@/api/customer'
import { getStatistics } from '@/api/dashboard'
const userStore = useUserStore()
@ -77,14 +78,19 @@ const stats = ref({
dealerCount: 0
})
//
onMounted(() => {
stats.value = {
customerCount: 5,
reportCount: 0,
pendingCount: 0,
dealerCount: 3
//
const fetchStatistics = async () => {
try {
const res = await getStatistics()
stats.value = res
} catch (error) {
console.error('加载统计数据失败', error)
ElMessage.error('加载统计数据失败')
}
}
onMounted(() => {
fetchStatistics()
})
</script>

View File

@ -16,8 +16,8 @@
<el-form-item label="经销商名称">
<el-input v-model="queryForm.name" placeholder="请输入经销商名称" clearable />
</el-form-item>
<el-form-item label="经销商编码">
<el-input v-model="queryForm.code" placeholder="请输入经销商编码" clearable />
<el-form-item label="经销商账号">
<el-input v-model="queryForm.code" placeholder="请输入经销商账号" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryForm.status" placeholder="请选择状态" clearable>
@ -37,7 +37,7 @@
<!-- 数据表格 -->
<el-table :data="tableData" border stripe v-loading="loading">
<el-table-column prop="name" label="经销商名称" />
<el-table-column prop="code" label="经销商编码" />
<el-table-column prop="code" label="经销商账号" />
<el-table-column prop="contactPerson" label="联系人" />
<el-table-column prop="contactPhone" label="联系电话" />
<el-table-column prop="email" label="邮箱" />
@ -49,9 +49,10 @@
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="180" />
<el-table-column label="操作" width="180" fixed="right">
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="warning" @click="handleResetPassword(row)">重置密码</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
@ -69,8 +70,8 @@
<el-form-item label="经销商名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入经销商名称" />
</el-form-item>
<el-form-item label="经销商编码" prop="code">
<el-input v-model="formData.code" placeholder="请输入经销商编码" />
<el-form-item label="经销商账号" prop="code">
<el-input v-model="formData.code" placeholder="请输入经销商账号" />
</el-form-item>
<el-form-item label="联系人" prop="contactPerson">
<el-input v-model="formData.contactPerson" placeholder="请输入联系人" />
@ -87,12 +88,57 @@
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="初始密码" prop="password" v-if="!formData.id">
<el-input
v-model="formData.password"
type="password"
placeholder="请输入初始密码不填默认为123456"
show-password
/>
<div style="color: #909399; font-size: 12px; margin-top: 4px">
长度6-20不填则默认密码为 123456
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
<!-- 重置密码对话框 -->
<el-dialog
v-model="resetPasswordDialogVisible"
title="重置密码"
width="500px"
@close="handleResetPasswordDialogClose"
>
<el-form ref="resetPasswordFormRef" :model="resetPasswordForm" :rules="resetPasswordRules" label-width="100px">
<el-form-item label="经销商名称">
<el-input v-model="resetPasswordForm.dealerName" disabled />
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input
v-model="resetPasswordForm.newPassword"
type="password"
placeholder="请输入新密码6-20位"
show-password
/>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input
v-model="resetPasswordForm.confirmPassword"
type="password"
placeholder="请再次输入新密码"
show-password
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="resetPasswordDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleResetPasswordSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
@ -100,13 +146,17 @@
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { getDealerList, createDealer, updateDealer, deleteDealer } from '@/api/dealer'
import { resetPassword } from '@/api/auth'
import { hashPassword } from '@/utils/crypto'
import type { Dealer } from '@/types'
const loading = ref(false)
const tableData = ref<Dealer[]>([])
const dialogVisible = ref(false)
const resetPasswordDialogVisible = ref(false)
const dialogTitle = computed(() => (formData.id ? '编辑经销商' : '新增经销商'))
const formRef = ref<FormInstance>()
const resetPasswordFormRef = ref<FormInstance>()
const queryForm = reactive({
name: '',
@ -114,20 +164,62 @@ const queryForm = reactive({
status: undefined as number | undefined
})
const formData = reactive<Partial<Dealer> & { id?: number }>({
const formData = reactive<Partial<Dealer> & { id?: number; password?: string }>({
name: '',
code: '',
contactPerson: '',
contactPhone: '',
email: '',
status: 1
status: 1,
password: ''
})
const rules: FormRules = {
name: [{ required: true, message: '请输入经销商名称', trigger: 'blur' }],
code: [{ required: true, message: '请输入经销商编码', trigger: 'blur' }],
code: [{ required: true, message: '请输入经销商账号', trigger: 'blur' }],
contactPerson: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
contactPhone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }]
contactPhone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
password: [
{
validator: (rule, value, callback) => {
//
if (value && (value.length < 6 || value.length > 20)) {
callback(new Error('密码长度必须在6-20位之间'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}
//
const resetPasswordForm = reactive({
userId: 0,
dealerName: '',
newPassword: '',
confirmPassword: ''
})
const resetPasswordRules: FormRules = {
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, max: 20, message: '新密码长度必须在6-20位之间', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value !== resetPasswordForm.newPassword) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}
//
@ -170,7 +262,15 @@ const handleEdit = (row: Dealer) => {
//
const handleDelete = async (row: Dealer) => {
try {
await ElMessageBox.confirm('确定要删除该经销商吗?', '提示', { type: 'warning' })
await ElMessageBox.confirm(
'确定要删除该经销商吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
await deleteDealer(row.id)
ElMessage.success('删除成功')
fetchData()
@ -201,6 +301,46 @@ const handleSubmit = async () => {
})
}
//
const handleResetPassword = (row: Dealer) => {
resetPasswordForm.userId = row.userId
resetPasswordForm.dealerName = row.name
resetPasswordDialogVisible.value = true
}
//
const handleResetPasswordSubmit = async () => {
if (!resetPasswordFormRef.value) return
await resetPasswordFormRef.value.validate(async (valid) => {
if (valid) {
try {
// SHA-256
const hashedPassword = await hashPassword(resetPasswordForm.newPassword)
await resetPassword({
userId: resetPasswordForm.userId,
newPassword: hashedPassword
})
ElMessage.success('密码重置成功')
resetPasswordDialogVisible.value = false
} catch (error) {
console.error('重置密码失败', error)
}
}
})
}
//
const handleResetPasswordDialogClose = () => {
resetPasswordFormRef.value?.resetFields()
Object.assign(resetPasswordForm, {
userId: 0,
dealerName: '',
newPassword: '',
confirmPassword: ''
})
}
//
const handleDialogClose = () => {
formRef.value?.resetFields()
@ -210,7 +350,8 @@ const handleDialogClose = () => {
contactPerson: '',
contactPhone: '',
email: '',
status: 1
status: 1,
password: ''
})
delete formData.id
}

View File

@ -25,6 +25,10 @@
<el-icon><Shop /></el-icon>
<span>经销商管理</span>
</el-menu-item>
<el-menu-item v-if="userStore.isAdmin" index="/system-config">
<el-icon><Setting /></el-icon>
<span>系统配置</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-container>
@ -47,6 +51,10 @@
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="showChangePasswordDialog = true">
<el-icon><Lock /></el-icon>
修改密码
</el-dropdown-item>
<el-dropdown-item @click="handleLogout">
<el-icon><SwitchButton /></el-icon>
退出登录
@ -61,14 +69,55 @@
<router-view />
</el-main>
</el-container>
<!-- 修改密码对话框 -->
<el-dialog
v-model="showChangePasswordDialog"
title="修改密码"
width="500px"
@close="handleDialogClose"
>
<el-form ref="formRef" :model="passwordForm" :rules="passwordRules" label-width="100px">
<el-form-item label="原密码" prop="oldPassword">
<el-input
v-model="passwordForm.oldPassword"
type="password"
placeholder="请输入原密码"
show-password
/>
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input
v-model="passwordForm.newPassword"
type="password"
placeholder="请输入新密码6-20位"
show-password
/>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input
v-model="passwordForm.confirmPassword"
type="password"
placeholder="请再次输入新密码"
show-password
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showChangePasswordDialog = false">取消</el-button>
<el-button type="primary" @click="handleChangePassword">确定</el-button>
</template>
</el-dialog>
</el-container>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessageBox } from 'element-plus'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { useUserStore } from '@/stores/user'
import { changePassword } from '@/api/auth'
import { hashPassword } from '@/utils/crypto'
const route = useRoute()
const router = useRouter()
@ -77,11 +126,81 @@ const userStore = useUserStore()
const activeMenu = computed(() => route.path)
const currentPageTitle = computed(() => route.meta.title as string || '首页')
//
const showChangePasswordDialog = ref(false)
const formRef = ref<FormInstance>()
const passwordForm = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
const passwordRules: FormRules = {
oldPassword: [{ required: true, message: '请输入原密码', trigger: 'blur' }],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, max: 20, message: '新密码长度必须在6-20位之间', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value !== passwordForm.newPassword) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}
const handleChangePassword = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
try {
// SHA-256
const hashedOldPassword = await hashPassword(passwordForm.oldPassword)
const hashedNewPassword = await hashPassword(passwordForm.newPassword)
await changePassword({
oldPassword: hashedOldPassword,
newPassword: hashedNewPassword
})
ElMessage.success('密码修改成功,请重新登录')
showChangePasswordDialog.value = false
// 退
userStore.logout()
router.push('/login')
} catch (error) {
console.error('修改密码失败', error)
}
}
})
}
const handleDialogClose = () => {
formRef.value?.resetFields()
Object.assign(passwordForm, {
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
}
const handleLogout = async () => {
try {
await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
await ElMessageBox.confirm(
'确定要退出登录吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
}
)
userStore.logout()
router.push('/login')
} catch {

View File

@ -36,6 +36,7 @@ import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { useUserStore } from '@/stores/user'
import { hashPassword } from '@/utils/crypto'
const router = useRouter()
const userStore = useUserStore()
@ -59,7 +60,12 @@ const handleLogin = async () => {
if (valid) {
loading.value = true
try {
await userStore.login(loginForm)
// SHA-256
const hashedPassword = await hashPassword(loginForm.password)
await userStore.login({
username: loginForm.username,
password: hashedPassword
})
ElMessage.success('登录成功')
router.push('/')
} catch (error) {

View File

@ -3,7 +3,12 @@
<el-card>
<template #header>
<div class="card-header">
<div>
<span>报备管理</span>
<el-tag type="info" size="small" style="margin-left: 10px">
保护期{{ protectDays }}
</el-tag>
</div>
<el-button type="primary" @click="handleAdd" v-if="!userStore.isAdmin">
<el-icon><Plus /></el-icon>
提交报备
@ -49,7 +54,7 @@
</el-table-column>
<el-table-column prop="protectEndDate" label="保护期截止" width="180">
<template #default="{ row }">
{{ row.protectEndDate || '-' }}
{{ formatDate(row.protectEndDate) }}
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="180" />
@ -83,6 +88,8 @@
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
prev-text="上一页"
next-text="下一页"
@size-change="fetchData"
@current-change="fetchData"
style="margin-top: 20px; justify-content: flex-end"
@ -157,10 +164,13 @@ import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'elem
import { useUserStore } from '@/stores/user'
import { getReportPage, createReport, auditReport, withdrawReport } from '@/api/report'
import { searchCustomerByName } from '@/api/customer'
import { getConfigValue } from '@/api/system'
import type { Report, ReportForm } from '@/types'
const userStore = useUserStore()
const protectDays = ref<number>(90)
const loading = ref(false)
const tableData = ref<Report[]>([])
const total = ref(0)
@ -193,6 +203,12 @@ const rules: FormRules = {
customerId: [{ required: true, message: '请选择客户', trigger: 'change' }]
}
//
const formatDate = (dateStr: string | null | undefined) => {
if (!dateStr) return '-'
return dateStr.split(' ')[0] //
}
//
const fetchData = async () => {
loading.value = true
@ -251,7 +267,7 @@ const handleView = (row: Report) => {
<p><strong>报备说明</strong>${row.description || '-'}</p>
<p><strong>状态</strong>${row.statusDesc}</p>
<p><strong>驳回原因</strong>${row.rejectReason || '-'}</p>
<p><strong>保护期</strong>${row.protectStartDate || '-'} ~ ${row.protectEndDate || '-'}</p>
<p><strong>保护期</strong>${formatDate(row.protectStartDate)} ~ ${formatDate(row.protectEndDate)}</p>
</div>
`,
'报备详情',
@ -273,7 +289,15 @@ const handleAudit = (row: Report) => {
//
const handleWithdraw = async (row: Report) => {
try {
await ElMessageBox.confirm('确定要撤回该报备吗?', '提示', { type: 'warning' })
await ElMessageBox.confirm(
'确定要撤回该报备吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
await withdrawReport(row.id)
ElMessage.success('撤回成功')
fetchData()
@ -326,7 +350,18 @@ const handleDialogClose = () => {
customerOptions.value = []
}
//
const fetchProtectDays = async () => {
try {
const res = await getConfigValue('report.protect.days')
protectDays.value = parseInt(res) || 90
} catch (error) {
console.error('获取配置失败', error)
}
}
onMounted(() => {
fetchProtectDays()
fetchData()
})
</script>

View File

@ -0,0 +1,146 @@
<template>
<div class="system-config-page">
<el-card>
<template #header>
<div class="card-header">
<span>系统配置管理</span>
<el-button type="primary" @click="handleSave" :loading="saving">
<el-icon><Check /></el-icon>
保存配置
</el-button>
</div>
</template>
<el-alert
title="提示"
type="warning"
:closable="false"
style="margin-bottom: 20px"
>
修改配置后立即生效请谨慎操作保护期天数的修改仅影响新生成的报备
</el-alert>
<el-form :model="formData" label-width="150px">
<el-divider content-position="left">报备配置</el-divider>
<el-form-item label="保护期天数">
<el-input-number
v-model="formData['report.protect.days']"
:min="1"
:max="365"
:step="1"
controls-position="right"
style="width: 200px"
/>
<span style="margin-left: 10px; color: #909399"></span>
<div style="margin-top: 5px; color: #909399; font-size: 12px">
报备审核通过后客户的保护时长修改仅影响新生成的报备
</div>
</el-form-item>
<el-form-item label="允许重叠报备">
<el-switch
v-model="formData['report.allow.overlap']"
active-text="是"
inactive-text="否"
/>
<div style="margin-top: 5px; color: #F56C6C; font-size: 12px">
警告生产环境必须设置为"否"否则会导致同一个客户被多个经销商报备
</div>
</el-form-item>
<el-divider content-position="left">当前配置状态</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="当前保护期天数">
{{ configs.find(c => c.configKey === 'report.protect.days')?.configValue || 90 }}
</el-descriptions-item>
<el-descriptions-item label="允许重叠报备">
<el-tag :type="configs.find(c => c.configKey === 'report.allow.overlap')?.configValue === 'true' ? 'danger' : 'success'">
{{ configs.find(c => c.configKey === 'report.allow.overlap')?.configValue === 'true' ? '是(危险)' : '否(安全)' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Check } from '@element-plus/icons-vue'
import { getAllConfigs, batchUpdateConfigs } from '@/api/system'
import type { SystemConfig } from '@/types'
const configs = ref<SystemConfig[]>([])
const saving = ref(false)
const formData = reactive<Record<string, any>>({
'report.protect.days': 90,
'report.allow.overlap': false
})
//
const fetchConfigs = async () => {
try {
const res = await getAllConfigs()
configs.value = res
//
res.forEach(config => {
if (config.configType === 'integer') {
formData[config.configKey] = parseInt(config.configValue)
} else if (config.configType === 'boolean') {
formData[config.configKey] = config.configValue === 'true'
} else {
formData[config.configKey] = config.configValue
}
})
} catch (error) {
console.error('加载配置失败', error)
ElMessage.error('加载配置失败')
}
}
//
const handleSave = async () => {
saving.value = true
try {
const configMap: Record<string, string> = {}
Object.keys(formData).forEach(key => {
const value = formData[key]
if (typeof value === 'boolean') {
configMap[key] = String(value)
} else {
configMap[key] = String(value)
}
})
await batchUpdateConfigs(configMap)
ElMessage.success('保存成功')
fetchConfigs()
} catch (error) {
console.error('保存失败', error)
ElMessage.error('保存失败')
} finally {
saving.value = false
}
}
onMounted(() => {
fetchConfigs()
})
</script>
<style scoped>
.system-config-page {
width: 100%;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

1
tmpclaude-62fc-cwd Normal file
View File

@ -0,0 +1 @@
/e/boyun-workspace/by-crm