项目名调整,功能优化

This commit is contained in:
2026-01-26 22:20:27 +08:00
parent c6e5e655f9
commit 50275265ac
25 changed files with 413 additions and 66 deletions

4
.gitignore vendored
View File

@@ -50,6 +50,6 @@ desktop.ini
/data/ /data/
/picture/ /picture/
/config.yaml /config.yaml
/bing_daily_image.db /bing_paper.db
/req.txt /req.txt
/BingDailyImage /BingPaper

View File

@@ -5,15 +5,15 @@ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
RUN go build -o BingDailyImage . RUN go build -o BingPaper .
FROM alpine:latest FROM alpine:latest
WORKDIR /app WORKDIR /app
COPY --from=builder /app/BingDailyImage . COPY --from=builder /app/BingPaper .
RUN mkdir -p data RUN mkdir -p data
COPY --from=builder /app/config.example.yaml ./data/config.yaml COPY --from=builder /app/config.example.yaml ./data/config.yaml
COPY --from=builder /app/web ./web COPY --from=builder /app/web ./web
EXPOSE 8080 EXPOSE 8080
ENTRYPOINT ["./BingDailyImage"] ENTRYPOINT ["./BingPaper"]

View File

@@ -1,4 +1,4 @@
# BingDailyImage # BingPaper
必应每日一图抓取、存储、多分辨率管理与公共 API 服务。 必应每日一图抓取、存储、多分辨率管理与公共 API 服务。
@@ -71,10 +71,10 @@ go run .
```bash ```bash
# 构建二进制 # 构建二进制
go build -o BingDailyImage . go build -o BingPaper .
# 构建 Docker 镜像 # 构建 Docker 镜像
docker build -t bing-daily-image . docker build -t bing-paper .
``` ```
## 许可证 ## 许可证

47
config.example.yaml Normal file
View File

@@ -0,0 +1,47 @@
server:
port: 8080
base_url: ""
log:
level: info
api:
mode: local # local | redirect
cron:
enabled: true
daily_spec: "0 10 * * *"
retention:
days: 30
db:
type: sqlite # sqlite | mysql | postgres
dsn: data/bing_paper.db
storage:
type: local # local | s3 | webdav
local:
root: data/picture
s3:
endpoint: ""
region: ""
bucket: ""
access_key: ""
secret_key: ""
public_url_prefix: ""
force_path_style: false
webdav:
url: ""
username: ""
password: ""
public_url_prefix: ""
admin:
password_bcrypt: "$2a$10$fYHPeWHmwObephJvtlyH1O8DIgaLk5TINbi9BOezo2M8cSjmJchka" # 默认密码: admin123
token:
default_ttl: 168h
feature:
write_daily_files: true

View File

@@ -189,6 +189,66 @@ const docTemplate = `{
} }
} }
}, },
"/admin/password": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "验证旧密码并设置新密码,自动更新配置文件",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "修改管理员密码",
"parameters": [
{
"description": "修改密码请求",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.ChangePasswordRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/admin/tokens": { "/admin/tokens": {
"get": { "get": {
"security": [ "security": [
@@ -747,6 +807,21 @@ const docTemplate = `{
} }
} }
}, },
"handlers.ChangePasswordRequest": {
"type": "object",
"required": [
"new_password",
"old_password"
],
"properties": {
"new_password": {
"type": "string"
},
"old_password": {
"type": "string"
}
}
},
"handlers.CreateTokenRequest": { "handlers.CreateTokenRequest": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -835,7 +910,7 @@ var SwaggerInfo = &swag.Spec{
Host: "localhost:8080", Host: "localhost:8080",
BasePath: "/api/v1", BasePath: "/api/v1",
Schemes: []string{}, Schemes: []string{},
Title: "BingDailyImage API", Title: "BingPaper API",
Description: "必应每日一图抓取、存储、管理与公共 API 服务。", Description: "必应每日一图抓取、存储、管理与公共 API 服务。",
InfoInstanceName: "swagger", InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate, SwaggerTemplate: docTemplate,

View File

@@ -2,7 +2,7 @@
"swagger": "2.0", "swagger": "2.0",
"info": { "info": {
"description": "必应每日一图抓取、存储、管理与公共 API 服务。", "description": "必应每日一图抓取、存储、管理与公共 API 服务。",
"title": "BingDailyImage API", "title": "BingPaper API",
"contact": {}, "contact": {},
"version": "1.0" "version": "1.0"
}, },
@@ -183,6 +183,66 @@
} }
} }
}, },
"/admin/password": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "验证旧密码并设置新密码,自动更新配置文件",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "修改管理员密码",
"parameters": [
{
"description": "修改密码请求",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.ChangePasswordRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/admin/tokens": { "/admin/tokens": {
"get": { "get": {
"security": [ "security": [
@@ -741,6 +801,21 @@
} }
} }
}, },
"handlers.ChangePasswordRequest": {
"type": "object",
"required": [
"new_password",
"old_password"
],
"properties": {
"new_password": {
"type": "string"
},
"old_password": {
"type": "string"
}
}
},
"handlers.CreateTokenRequest": { "handlers.CreateTokenRequest": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@@ -121,6 +121,16 @@ definitions:
username: username:
type: string type: string
type: object type: object
handlers.ChangePasswordRequest:
properties:
new_password:
type: string
old_password:
type: string
required:
- new_password
- old_password
type: object
handlers.CreateTokenRequest: handlers.CreateTokenRequest:
properties: properties:
expires_at: expires_at:
@@ -172,7 +182,7 @@ host: localhost:8080
info: info:
contact: {} contact: {}
description: 必应每日一图抓取、存储、管理与公共 API 服务。 description: 必应每日一图抓取、存储、管理与公共 API 服务。
title: BingDailyImage API title: BingPaper API
version: "1.0" version: "1.0"
paths: paths:
/admin/cleanup: /admin/cleanup:
@@ -283,6 +293,44 @@ paths:
summary: 管理员登录 summary: 管理员登录
tags: tags:
- admin - admin
/admin/password:
post:
consumes:
- application/json
description: 验证旧密码并设置新密码,自动更新配置文件
parameters:
- description: 修改密码请求
in: body
name: request
required: true
schema:
$ref: '#/definitions/handlers.ChangePasswordRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties:
type: string
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"401":
description: Unauthorized
schema:
additionalProperties:
type: string
type: object
security:
- BearerAuth: []
summary: 修改管理员密码
tags:
- admin
/admin/tokens: /admin/tokens:
get: get:
description: 获取所有已创建的 API Token 列表 description: 获取所有已创建的 API Token 列表

6
go.mod
View File

@@ -1,4 +1,4 @@
module BingDailyImage module BingPaper
go 1.25.5 go 1.25.5
@@ -13,6 +13,7 @@ require (
github.com/chai2010/webp v1.4.0 // indirect github.com/chai2010/webp v1.4.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/disintegration/imaging v1.6.2 // indirect github.com/disintegration/imaging v1.6.2 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect
@@ -46,6 +47,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect github.com/quic-go/quic-go v0.54.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect
@@ -57,6 +59,7 @@ require (
github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect github.com/spf13/viper v1.21.0 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/studio-b12/gowebdav v0.12.0 // indirect github.com/studio-b12/gowebdav v0.12.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/swaggo/files v1.0.1 // indirect github.com/swaggo/files v1.0.1 // indirect
@@ -80,6 +83,7 @@ require (
golang.org/x/tools v0.40.0 // indirect golang.org/x/tools v0.40.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/mysql v1.6.0 // indirect gorm.io/driver/mysql v1.6.0 // indirect
gorm.io/driver/postgres v1.6.0 // indirect gorm.io/driver/postgres v1.6.0 // indirect
gorm.io/driver/sqlite v1.6.0 // indirect gorm.io/driver/sqlite v1.6.0 // indirect

5
go.sum
View File

@@ -21,6 +21,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSY
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
@@ -101,6 +102,7 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
@@ -133,6 +135,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/studio-b12/gowebdav v0.12.0 h1:kFRtQECt8jmVAvA6RHBz3geXUGJHUZA6/IKpOVUs5kM= github.com/studio-b12/gowebdav v0.12.0 h1:kFRtQECt8jmVAvA6RHBz3geXUGJHUZA6/IKpOVUs5kM=
github.com/studio-b12/gowebdav v0.12.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= github.com/studio-b12/gowebdav v0.12.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
@@ -220,6 +224,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=

View File

@@ -6,16 +6,16 @@ import (
"log" "log"
"os" "os"
"BingDailyImage/internal/config" "BingPaper/internal/config"
"BingDailyImage/internal/cron" "BingPaper/internal/cron"
"BingDailyImage/internal/http" "BingPaper/internal/http"
"BingDailyImage/internal/repo" "BingPaper/internal/repo"
"BingDailyImage/internal/service/fetcher" "BingPaper/internal/service/fetcher"
"BingDailyImage/internal/storage" "BingPaper/internal/storage"
"BingDailyImage/internal/storage/local" "BingPaper/internal/storage/local"
"BingDailyImage/internal/storage/s3" "BingPaper/internal/storage/s3"
"BingDailyImage/internal/storage/webdav" "BingPaper/internal/storage/webdav"
"BingDailyImage/internal/util" "BingPaper/internal/util"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/zap" "go.uber.org/zap"
@@ -93,7 +93,7 @@ func LogWelcomeInfo() {
} }
fmt.Println("\n---------------------------------------------------------") fmt.Println("\n---------------------------------------------------------")
fmt.Println(" BingDailyImage 服务已启动!") fmt.Println(" BingPaper 服务已启动!")
fmt.Printf(" - 首页地址: %s/\n", baseURL) fmt.Printf(" - 首页地址: %s/\n", baseURL)
fmt.Printf(" - 管理后台: %s/admin\n", baseURL) fmt.Printf(" - 管理后台: %s/admin\n", baseURL)
fmt.Printf(" - API 文档: %s/swagger/index.html\n", baseURL) fmt.Printf(" - API 文档: %s/swagger/index.html\n", baseURL)

View File

@@ -120,7 +120,7 @@ func Init(configPath string) error {
v.SetDefault("cron.daily_spec", "0 10 * * *") v.SetDefault("cron.daily_spec", "0 10 * * *")
v.SetDefault("retention.days", 30) v.SetDefault("retention.days", 30)
v.SetDefault("db.type", "sqlite") v.SetDefault("db.type", "sqlite")
v.SetDefault("db.dsn", "data/bing_daily_image.db") v.SetDefault("db.dsn", "data/bing_paper.db")
v.SetDefault("storage.type", "local") v.SetDefault("storage.type", "local")
v.SetDefault("storage.local.root", "data/picture") v.SetDefault("storage.local.root", "data/picture")
v.SetDefault("token.default_ttl", "168h") v.SetDefault("token.default_ttl", "168h")

View File

@@ -3,10 +3,10 @@ package cron
import ( import (
"context" "context"
"BingDailyImage/internal/config" "BingPaper/internal/config"
"BingDailyImage/internal/service/fetcher" "BingPaper/internal/service/fetcher"
"BingDailyImage/internal/service/image" "BingPaper/internal/service/image"
"BingDailyImage/internal/util" "BingPaper/internal/util"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
"go.uber.org/zap" "go.uber.org/zap"

View File

@@ -6,12 +6,13 @@ import (
"strconv" "strconv"
"time" "time"
"BingDailyImage/internal/config" "BingPaper/internal/config"
"BingDailyImage/internal/service/fetcher" "BingPaper/internal/service/fetcher"
"BingDailyImage/internal/service/image" "BingPaper/internal/service/image"
"BingDailyImage/internal/service/token" "BingPaper/internal/service/token"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
) )
type LoginRequest struct { type LoginRequest struct {
@@ -109,6 +110,55 @@ type UpdateTokenRequest struct {
Disabled bool `json:"disabled"` Disabled bool `json:"disabled"`
} }
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required"`
}
// ChangePassword 修改管理员密码
// @Summary 修改管理员密码
// @Description 验证旧密码并设置新密码,自动更新配置文件
// @Tags admin
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param request body ChangePasswordRequest true "修改密码请求"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /admin/password [post]
func ChangePassword(c *gin.Context) {
var req ChangePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
cfg := config.GetConfig()
// 验证旧密码
err := bcrypt.CompareHashAndPassword([]byte(cfg.Admin.PasswordBcrypt), []byte(req.OldPassword))
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid old password"})
return
}
// 生成新密码 Hash
hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash password"})
return
}
// 更新配置
cfg.Admin.PasswordBcrypt = string(hash)
if err := config.SaveConfig(cfg); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save config"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ok", "message": "password updated successfully"})
}
// UpdateToken 更新 Token 状态 // UpdateToken 更新 Token 状态
// @Summary 更新 Token 状态 // @Summary 更新 Token 状态
// @Description 启用或禁用指定的 API Token // @Description 启用或禁用指定的 API Token

View File

@@ -6,10 +6,10 @@ import (
"io" "io"
"net/http" "net/http"
"BingDailyImage/internal/config" "BingPaper/internal/config"
"BingDailyImage/internal/model" "BingPaper/internal/model"
"BingDailyImage/internal/service/image" "BingPaper/internal/service/image"
"BingDailyImage/internal/storage" "BingPaper/internal/storage"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )

View File

@@ -4,7 +4,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"BingDailyImage/internal/service/token" "BingPaper/internal/service/token"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )

View File

@@ -1,9 +1,9 @@
package http package http
import ( import (
_ "BingDailyImage/docs" _ "BingPaper/docs"
"BingDailyImage/internal/http/handlers" "BingPaper/internal/http/handlers"
"BingDailyImage/internal/http/middleware" "BingPaper/internal/http/middleware"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files" swaggerFiles "github.com/swaggo/files"
@@ -50,6 +50,8 @@ func SetupRouter() *gin.Engine {
authorized.PATCH("/tokens/:id", handlers.UpdateToken) authorized.PATCH("/tokens/:id", handlers.UpdateToken)
authorized.DELETE("/tokens/:id", handlers.DeleteToken) authorized.DELETE("/tokens/:id", handlers.DeleteToken)
authorized.POST("/password", handlers.ChangePassword)
authorized.GET("/config", handlers.GetConfig) authorized.GET("/config", handlers.GetConfig)
authorized.PUT("/config", handlers.UpdateConfig) authorized.PUT("/config", handlers.UpdateConfig)

View File

@@ -1,9 +1,9 @@
package repo package repo
import ( import (
"BingDailyImage/internal/config" "BingPaper/internal/config"
"BingDailyImage/internal/model" "BingPaper/internal/model"
"BingDailyImage/internal/util" "BingPaper/internal/util"
"fmt" "fmt"
"go.uber.org/zap" "go.uber.org/zap"

View File

@@ -13,11 +13,11 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"BingDailyImage/internal/config" "BingPaper/internal/config"
"BingDailyImage/internal/model" "BingPaper/internal/model"
"BingDailyImage/internal/repo" "BingPaper/internal/repo"
"BingDailyImage/internal/storage" "BingPaper/internal/storage"
"BingDailyImage/internal/util" "BingPaper/internal/util"
"github.com/chai2010/webp" "github.com/chai2010/webp"
"github.com/disintegration/imaging" "github.com/disintegration/imaging"

View File

@@ -5,11 +5,11 @@ import (
"fmt" "fmt"
"time" "time"
"BingDailyImage/internal/config" "BingPaper/internal/config"
"BingDailyImage/internal/model" "BingPaper/internal/model"
"BingDailyImage/internal/repo" "BingPaper/internal/repo"
"BingDailyImage/internal/storage" "BingPaper/internal/storage"
"BingDailyImage/internal/util" "BingPaper/internal/util"
"go.uber.org/zap" "go.uber.org/zap"
) )

View File

@@ -6,9 +6,9 @@ import (
"errors" "errors"
"time" "time"
"BingDailyImage/internal/config" "BingPaper/internal/config"
"BingDailyImage/internal/model" "BingPaper/internal/model"
"BingDailyImage/internal/repo" "BingPaper/internal/repo"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@@ -51,7 +51,21 @@ func Login(password string) (*model.Token, error) {
} }
ttl := config.GetTokenTTL() ttl := config.GetTokenTTL()
return CreateToken("login-token", time.Now().Add(ttl)) expiresAt := time.Now().Add(ttl)
name := "login-token"
// 如果已存在同名 token则刷新时间并返回
var t model.Token
if err := repo.DB.Where("name = ?", name).First(&t).Error; err == nil {
t.ExpiresAt = expiresAt
t.Disabled = false
if err := repo.DB.Save(&t).Error; err != nil {
return nil, err
}
return &t, nil
}
return CreateToken(name, expiresAt)
} }
func ListTokens() ([]model.Token, error) { func ListTokens() ([]model.Token, error) {

View File

@@ -6,7 +6,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"BingDailyImage/internal/storage" "BingPaper/internal/storage"
) )
type LocalStorage struct { type LocalStorage struct {

View File

@@ -6,7 +6,7 @@ import (
"io" "io"
"strings" "strings"
"BingDailyImage/internal/storage" "BingPaper/internal/storage"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"

View File

@@ -7,7 +7,7 @@ import (
"path" "path"
"strings" "strings"
"BingDailyImage/internal/storage" "BingPaper/internal/storage"
"github.com/studio-b12/gowebdav" "github.com/studio-b12/gowebdav"
) )

View File

@@ -3,14 +3,14 @@ package main
import ( import (
"fmt" "fmt"
"BingDailyImage/internal/bootstrap" "BingPaper/internal/bootstrap"
"BingDailyImage/internal/config" "BingPaper/internal/config"
"BingDailyImage/internal/util" "BingPaper/internal/util"
"go.uber.org/zap" "go.uber.org/zap"
) )
// @title BingDailyImage API // @title BingPaper API
// @version 1.0 // @version 1.0
// @description 必应每日一图抓取、存储、管理与公共 API 服务。 // @description 必应每日一图抓取、存储、管理与公共 API 服务。
// @host localhost:8080 // @host localhost:8080

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BingDailyImage - 必应每日一图</title> <title>BingPaper - 必应每日一图</title>
<style> <style>
:root { :root {
--primary-color: #007bff; --primary-color: #007bff;
@@ -67,7 +67,7 @@
<body> <body>
<nav> <nav>
<a href="/" class="logo">BingDailyImage</a> <a href="/" class="logo">BingPaper</a>
<div> <div>
<a href="/">首页</a> <a href="/">首页</a>
<a href="/admin">管理</a> <a href="/admin">管理</a>
@@ -138,6 +138,13 @@
<button onclick="triggerFetch()" style="margin-bottom: 10px;">手动抓取 (最近8天)</button> <button onclick="triggerFetch()" style="margin-bottom: 10px;">手动抓取 (最近8天)</button>
<button class="secondary" onclick="triggerCleanup()" style="margin-bottom: 10px;">手动清理旧图</button> <button class="secondary" onclick="triggerCleanup()" style="margin-bottom: 10px;">手动清理旧图</button>
</div> </div>
<div class="admin-card">
<h3>修改密码</h3>
<input type="password" id="old-password" placeholder="旧密码" style="margin-bottom: 10px;">
<input type="password" id="new-password" placeholder="新密码" style="margin-bottom: 10px;">
<button onclick="changePassword()">提交修改</button>
</div>
<div class="admin-card"> <div class="admin-card">
<h3>今日图预览</h3> <h3>今日图预览</h3>
@@ -354,6 +361,26 @@
alert('清理任务已启动'); alert('清理任务已启动');
} }
async function changePassword() {
const oldPassword = document.getElementById('old-password').value;
const newPassword = document.getElementById('new-password').value;
if (!oldPassword || !newPassword) return alert('请输入完整信息');
const resp = await fetch('/api/v1/admin/password', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify({ old_password: oldPassword, new_password: newPassword })
});
const data = await resp.json();
if (resp.ok) {
alert('密码修改成功,请重新登录');
logout();
} else {
alert('修改失败: ' + (data.error || '未知错误'));
}
}
// 初始化 // 初始化
router(); router();
</script> </script>