Compare commits

...
920 Commits
Author SHA1 Message Date
wenjianzhang ec10917272 Merge pull request #947 from go-admin-team/fix/915-scheduler-lease
fix🐛: make the job scheduler single-writer with a database lease
2026-09-22 22:57:21 +08:00
wenjianzhang b8af16baf0 Merge pull request #946 from go-admin-team/test/job-remove-goroutine-leak
test: fail when Remove parks a goroutine on an abandoned channel
2026-09-22 22:54:55 +08:00
wenjianzhang 92987cdee3 Merge pull request #943 from go-admin-team/feat/010-generator-vue3
feat: generate Vue 3 pages from the code generator (PRD 010)
2026-09-22 21:42:43 +08:00
zhangwenjian df98ffb5c5 docs📝: say what a second replica now does to the scheduler
The note told the reader the scheduler stood in the way of raising the
replica count. It no longer does: one enabled job fires once however many
pods there are, a pod that loses the lease stops scheduling, and one that
exits hands the lease back rather than making its successor wait it out.

The shared log volume still does stand in the way, and that is now the only
thing the note asks for before the number goes up.
2026-09-20 18:31:44 +08:00
zhangwenjian acc9378283 fix🐛: schedule jobs only while this instance holds the lease
A second instance pointed at the same database did not divide the work, it
overwrote it. Every instance registered the whole enabled list into its own
cron.Cron, and startup ran `UPDATE sys_job SET entry_id = 0 WHERE entry_id
> 0` across the entire table, so the newest process erased the ids the
previous one wrote and put its own over the top. Neither symptom logged
anything: an enabled job fired once per instance, and stopping one from the
UI removed an entry from the wrong process and answered 200 either way.

A supervisor per tenant now keeps the scheduler in step with the lease.
Holding it is not decided once at startup, because both of the other
answers are wrong for longer than a moment:

  - an instance that never got the lease keeps asking, so the death of the
    holder does not stop the jobs until somebody restarts a process by hand;
  - an instance that holds it stops scheduling as soon as its lease has
    lapsed, because a holder still scheduling after the lease has gone
    elsewhere is the two-schedulers defect reached from the other side.

A failed renewal is not a lost lease. The scheduler keeps running until the
lease could actually have expired: a database that is briefly unreachable
must not stop the jobs, and cannot have handed them to anyone else, because
nobody else can reach it to take the lease either.

Stopping is no longer arranged by startCrontab. A scheduler now stops for
two different reasons and only the supervisor knows which - and since a
start happens every time the lease is taken, registering a shutdown
callback there would add one per leadership change for the life of the
process, because SetShutdown appends.

sys_job.entry_id keeps its meaning. This does not distribute the jobs and is
not meant to: the HTTP side scales, the scheduler stays single-writer.

Fixes #915.
2026-09-20 18:31:44 +08:00
zhangwenjian b4b5bc5b3a feat: a database lease the schedulers compete for
One row per database, taken and renewed by single UPDATE statements whose
RowsAffected the database decides. Nothing uses it yet; the scheduler is
wired to it next.

The two timestamps are epoch milliseconds in a BIGINT rather than timestamp
columns, which is the one decision here worth explaining. A timestamp does
not survive the trip through a driver unchanged: read over go-admin's own
`parseTime=True&loc=Local` DSN, MySQL's UTC_TIMESTAMP arrives relabelled as
local time, and on a UTC+8 host every lease is eight hours out. It is
invisible to any test that compares the lease against itself, because each
instance's own arithmetic stays self-consistent - only the comparison
between two instances is wrong, which is the only comparison that matters.
An integer has no timezone for a driver to apply.

The migration seeds the row free. There is no insert path at runtime, so two
instances starting together cannot race to create the row they are both
trying to claim, and neither has to tell a duplicate-key error apart from a
real one in whichever driver it is running against. A missing row is
therefore reported rather than recovered from: silently never scheduling
anywhere is the worse failure.

The current-time expression differs per dialect and all four are covered:
MySQL, PostgreSQL and SQL Server against real servers, SQLite by default.
2026-09-20 18:31:44 +08:00
zhangwenjian 27ad988fd7 ci👷: run the tests against MySQL too
MySQL is the dialect most installations run and the only registered driver
with no service here. The scheduler lease that follows reads the database's
clock, and the first implementation read it as a timestamp: over go-admin's
own `parseTime=True&loc=Local` DSN, MySQL's UTC_TIMESTAMP comes back
relabelled as local time, so on any host that is not UTC every lease was one
zone offset out. Every assertion that compared the lease only against itself
still passed, and the three dialects already here could not see it.

The DSN keeps loc=Local on purpose: it is what config/settings.yml ships. A
DSN here that quietly differed would test a configuration nobody runs.
2026-09-20 18:31:44 +08:00
zhangwenjian effc3a3e69 Merge branches 'feat/010-gen-plumbing' and 'feat/010-vue3-template' into integ/010-all 2026-09-19 21:21:25 +08:00
zhangwenjian 08f789737f fix🐛: escape the literal T in the datetime value-format
value-format="YYYY-MM-DDTHH:mm:ssZ" works today only because dayjs does
not currently give T a format-token meaning, so it passes through as a
literal character -- an accident of the current token table, not
something this string declares. Escaped it to YYYY-MM-DD[T]HH:mm:ssZ,
dayjs's own syntax for "this character, verbatim, not a token": produces
byte-for-byte the same output today (confirmed below) and stops
depending on T staying meaningless in a future dayjs version.

Re-verified both directions against the escaped string, and did so
against the real el-date-picker component this time rather than dayjs
alone: mounted element-plus's actual ElDatePicker with
value-format="YYYY-MM-DD[T]HH:mm:ssZ" (copied from a real rendering of
this fixed template, not retyped) and confirmed it renders a real,
non-blank date -- not "Invalid Date" -- when its modelValue is set to
either shape Go's encoding/json actually sends (a numeric offset or a
literal Z for UTC), and that both render identically since they are the
same instant. Submission was re-checked through the same dayjs call the
component itself makes to format a picked value. A fourth check formats
the same instant with both the old and the escaped string and asserts
they are equal, so this suite would have caught the difference if the
escape had changed anything instead of just hardening it.
2026-09-19 21:21:01 +08:00
zhangwenjian f57bf5d61d fix🐛: query-less table's Query type broke FK dropdown fetches (P0)
3625ce8 fixed the no-empty-object-type lint error by switching a
zero-IsQuery table's {ClassName}Query to `Record<string, never>`, the
same default useTable.ts's own `TQuery extends object = Record<string,
never>` uses. That default is safe there only because useTable.ts's one
internal `TQuery & PageQuery` goes through an `as` cast rather than a
structural check. Code that builds the object literal directly does not
get that protection - and vue.go.template's foreign-key dropdown fetch
does exactly that: `list{FkClass}({ pageIndex: 1, pageSize: 100 })`.

Record<string, never> is a mapped type over every string key, each
mapped to never, so `Record<string, never> & PageQuery` does not leave
PageQuery's own properties alone: pageIndex becomes `never & number`,
i.e. never, and no value can be passed for it - a straight type error,
not a lint warning, so the previous fix's pnpm lint pass did not catch
it. Trips on any query-less table referenced by a foreign key - a plain
lookup/dict table used as a dropdown source, an ordinary shape, not a
rare one.

Switched to Record<never, never>: a mapped type over the empty key
set, which behaves as the empty object type `{}` under intersection
(PageQuery's properties come through unchanged) while still satisfying
`TQuery extends object` and not tripping no-empty-object-type (it is a
generic instantiation, not a literal `{}` type annotation) - confirmed
all three separately before touching the template.

Verified with node 24.11.0: generated a real zero-IsQuery table, copied
its .ts into go-admin-ui alongside a throwaway file reproducing
vue.go.template's exact FK call site
(`await list{Class}({ pageIndex: 1, pageSize: 100 })`), and ran both
eslint and vue-tsc --noEmit - the type-check step lint alone cannot
cover, which is what let this through the first time. Confirmed red
first (swapped the generated file's Record<never, never> back to
Record<string, never>): vue-tsc reported the exact "Property 'pageIndex'
is incompatible with index signature" error. Restored the fix - both
clean.
2026-09-19 21:20:41 +08:00
zhangwenjian 143dbf19a2 Merge branches 'feat/010-gen-plumbing' and 'feat/010-vue3-template' into integ/010-all 2026-09-19 20:51:39 +08:00
zhangwenjian f6bd306d6d fix🐛: send datetime fields as RFC3339, not space-separated local time
Both date-pickers -- the search filter and the insert/edit form -- used
value-format="YYYY-MM-DD HH:mm:ss", which formats a picked instant as
e.g. "2026-09-19 12:30:00": no T separator, no offset. dto.go.template
declares every datetime column's InsertReq/UpdateReq field as
time.Time with a plain `json:"..."` tag (R6 leaves that file alone, so
there is no time_format tag to reach for instead), and encoding/json's
default (Un)MarshalJSON for time.Time only accepts RFC3339. The
generated form would submit new and edited datetime values in a shape
Go's JSON decoder cannot parse -- a runtime failure on every create and
update, with nothing in `pnpm type-check` or `pnpm lint` positioned to
see it: both check the request is well-typed TypeScript, not that the
string it produces is a string Go can read.

Changed both to value-format="YYYY-MM-DDTHH:mm:ssZ" -- dayjs's Z token
renders the picker's own local offset, which is what a zero-nanosecond
time.Time (anything without a database column storing sub-second
precision) round-trips to on either side of the wire; confirmed
separately against Go's actual json.Marshal/Unmarshal, not assumed from
the RFC.

The search filter needed the same fix, not just the form: GetPageReq
binds a `time.Time` query field via `form:"..."` (dto.go.template),
and gin's own default for an untagged time.Time binding is also
RFC3339 -- the same failure mode on the query side, one call the
report didn't name but the same root cause reaches.

This is a runtime behaviour change no compiler catches, so it was
verified as one: a Go program exercising encoding/json directly (not
assumed from reading the RFC) confirmed a zero-nanosecond time.Time
marshals to plain RFC3339 with no fractional seconds, and unmarshals
correctly from both a numeric offset and a literal Z. Separately, a
Node script loaded go-admin-ui's own installed dayjs 1.11.21 with the
customParseFormat plugin -- the same plugin element-plus's date-picker
extends dayjs with -- and called the same parseDate path date-picker
panel.mjs uses (time-picker/src/utils.ts, no strict flag passed, so
lenient parsing): formatting with this value-format produced a valid
submission string, and parsing either an offset or a literal-Z string
back with the same format produced a valid, correctly-valued date --
covering create, edit prefill, and update in the two directions that
matter (browser to Go, Go to browser) without needing a live backend.
2026-09-19 20:50:13 +08:00
zhangwenjian 3beb00143a fix🐛: give FK and dict options separate variable names
A column configured with both FkTableName and DictType -- and reaching
a branch of each, e.g. required + IsQuery=1 + IsInsert=1 with
HtmlType=radio -- had both blocks declare `const {JsonField}Options`:
the dict branch from useDict, the FK branch from ref(). TS2451,
Cannot redeclare block-scoped variable, and the page does not compile.

This is reachable precisely because FkTableName and DictType do not
exclude each other consistently: search, list and the form's select
branch check FK first and fall back to dict, but the form's radio
branch never looks at FK at all -- it was already established (the
$dictUsed/$fkUsed audit two commits back) that a radio column's dict
options are used regardless of whatever FkTableName says. A column
that is both radio and query-or-insert-select can legitimately need
both sources at once, under one shared name.

Renamed to {JsonField}DictOptions and {JsonField}FkOptions and updated
every consuming branch to the name that matches what it was already
branching on: FK branches (search select, form select, the list
column's Label function) read FkOptions; dict branches (search select,
form select, form radio, the list column's dictLabel call) read
DictOptions. Mechanical rename, no new conditions -- each site already
knew which source it wanted from its own if/else-if.

Verified with team-lead's exact repro (FkTableName + DictType +
IsQuery=1 + IsInsert=1 + HtmlType=radio) rendered through the real
template.Execute and checked against a throwaway go-admin-ui worktree
(deleted afterwards): TS2451 fired twice before this change, zero
after -- and the rendered file confirms the search select actually
reads kindFkOptions (FK wins search's priority) while the insert radio
reads kindDictOptions (radio never checks FK), so both sources are
live, not just declared. Re-ran every fixture from every previous
round alongside it; all stayed green. pnpm type-check and pnpm lint
both zero error, on Node 24.11.0.
2026-09-19 20:49:41 +08:00
zhangwenjian 7a52a50964 fix🐛: dedupe FK imports by target table and filter by actual use
Two columns pointing at the same foreign table -- owner and approver
both selecting from the same users table, say -- each triggered their
own `import { listX } from ...` / `import type { X } from ...` line,
which is a duplicate ES module import once both fire: TS2300. Nothing
here ever asked whether a target table had already been imported by an
earlier column, because nothing tracked target tables at all -- only
source columns, and "one FK-configured column" was never the same
thing as "one distinct target table".

The same import was also gated on the column having FkTableName set,
not on $fkUsed -- the condition the previous fix already applies to the
const declarations that read the import. A column carrying FK metadata
but reaching no query, list or insert-select branch imported a module
nothing in the file references.

text/template has no set to check membership in, so the dedup is a
nested range: a column only imports its target if no earlier,
equally-used column already claimed the same FkTableNameClass. $fkUsed
is recomputed for both the outer and the inner column rather than
factored out, since text/template has no way to carry a per-column
value computed in one range into a second, later range over the same
data.

Verified with a fixture carrying three columns pointing at the same
target table -- one read only from search, one only from the list, one
from neither -- rendered through the real template.Execute and checked
against a throwaway go-admin-ui worktree (deleted afterwards): before
this change, TS2300 fired six times (the function and the type, three
times over); after, exactly one import of each, and the unused third
column contributes neither. Re-ran the previous rounds' fixtures
alongside it; all stayed green. pnpm type-check and pnpm lint both zero
error, on Node 24.11.0.
2026-09-19 20:48:58 +08:00
zhangwenjian 630e13686c fix🐛: gate optional imports on where they're actually used, not on raw metadata
Every $hasX flag controlling an optional import matched "this column
carries the metadata" rather than "some rendered branch actually reads
it" -- necessary but not sufficient, since FkTableName/DictType lose to
each other by priority (FK wins search, list and the form's select
branch; the form's radio branch never checks FK at all) and a column
can carry either while being neither queryable, listed, nor an
insertable select/radio.

$hasDatetime was the reachable case integration testing found:
sys_tables.go assigns HtmlType "datetime" to any timestamp/datetime
column on import regardless of IsList, because GetList's audit-column
exclusion is a separate, later step editTable.vue never surfaces
created_at/updated_at through anyway. Nearly every real table has both,
so nearly every table imported DateCell without using it.

$hasFk and $hasDict had the identical shape one level down: the
per-column ref/onMounted/useDict declarations were gated on "this
column has FkTableName/DictType", not on whether the column reaches a
branch that reads the resulting Options ref -- an FK column used only
via search (no IsList, no insert-select) still declared a Label
function nothing calls, and a dict column used only in an insert radio
(no IsQuery, no IsList) still would have, had the two flags controlling
its import stayed as wide as the per-column check they were meant to
gate.

Rewrote both to a shared $dictUsed/$fkUsed condition, matching each
consuming branch's own guard term for term, and split the FK block's
Label function under its own IsList check -- Options can be needed for
search or the form's select without List ever being true. $hasDictList
already had this shape from the previous fix and needed no change.

Verified with two new fixtures, rendered through the real
template.Execute and checked against a throwaway go-admin-ui worktree
(deleted afterwards) with hand-written API-module stubs: a bare table
carrying only the standard created_at/updated_at pair -- confirmed red
on DateCell before this change, green after -- and a table exercising
every optional import through a path distinct from the ones the two
earlier verification rounds covered (a dict column read only from an
insert radio, an FK column read only from search, and a business
datetime column that IS listed, so DateCell still has to import when
the real thing needs it). Re-ran the three fixtures from the previous
two rounds alongside these two; all five stayed green. pnpm type-check
and pnpm lint both zero error, on Node 24.11.0.
2026-09-19 20:40:29 +08:00
zhangwenjian 05661e2f3e fix🐛: jsonField format check relaxed to any legal identifier
jsonFieldPattern copied businessName's rule (^[a-z][A-Za-z]+$: at least
two letters, no digits) on the theory that jsonField should tighten to
the same identifier shape. That does not hold: businessName is typed
by a person on genInfoForm.vue, so a strict pattern is a reasonable
guardrail on human input. jsonField is computed by the importer from
the column name (sys_tables.go's namelist/JsonField loop) - nobody
types it, so the same pattern only rejected names the importer
legitimately produces: a single-letter column ("x") or one whose last
segment ends in a digit ("address2", "a1") both collapse to a single
camelCase word with nothing left to re-capitalize, and both failed the
old check.

The blast radius is wider than "this one column can't be edited":
validateAndSanitizeColumns runs over every column on every Update, so
a table that merely contains one such column could not save any
config change at all, including edits with nothing to do with that
column.

Relaxed to ^[a-z][A-Za-z0-9]*$ - any legal JS/TS identifier starting
with a lowercase letter. Still rejects what has to be rejected: empty,
whitespace/punctuation, and leading-digit names, since those cannot be
unquoted object keys in the generated interface/lang file at all.
Uniqueness and the expression-content check on defaultValue are
unchanged - defaultValue is genuinely user-typed (F6's config page),
so tightening it was the right call to begin with; this was the only
place a human-input rule had been copied onto machine-generated data.

Verified against the real import path, not hand-typed jsonField values:
built a table with columns id/x/address2/a1 in a fake information_schema,
ran it through the real SysTable.Insert, confirmed the importer computes
exactly jsonField x/address2/a1, then submitted an update through the
real SysTable.Update changing only tableComment (nothing about those
columns). Confirmed red first - 500, "jsonField 格式不合法:\"x\"" - a
change unrelated to any of the three columns was rejected solely because
they existed on the table. Restored the fix - 200, "修改成功".
2026-09-19 20:37:01 +08:00
zhangwenjian 3625ce851b fix🐛: empty Query interface for zero-IsQuery tables trips no-empty-object-type
A plain display table with no search form (zero columns marked
IsQuery=1) is a normal shape, not an edge case - but ts.go.template's
Query interface only had a body when the range over .Columns found a
match, so it rendered `export interface {ClassName}Query {}`, which
@typescript-eslint/no-empty-object-type flags and pnpm lint fails on.

Falls back to `export type {ClassName}Query = Record<string, never>`
when no column qualifies - the same answer go-admin-ui's own
useTable.ts already gives this shape (`TQuery extends object =
Record<string, never>`), so it intersects with PageQuery the same way
an empty interface would have and callers do not need to special-case
a query-less table.

Audited the rest of this template for the same "zero of some optional
feature" shape while in here, since this is the third time a query-less/
select-less/whatever-less table has slipped through (dictLabel, DateCell,
this one):

  - a zero-column table: cannot occur - a table without at least a
    primary key column cannot exist to be imported from information_schema
    in the first place, so .Columns is never empty here.
  - a table with only its primary key column: the row interface still
    has one property (the pk); not empty, no lint issue.
  - a table where every column has IsInsert=0: does not affect this
    template - add{Class}/update{Class} both take the full row interface
    unconditionally (every column, not just IsInsert ones), so there is
    no column-count-dependent shape to go empty here.

Verified with node 24.11.0 (not the machine default): generated a
zero-IsQuery table and a with-IsQuery table, copied both into
go-admin-ui and ran eslint + vue-tsc --noEmit. Confirmed red first -
`export interface VerifyNoQueryQuery {}` failed eslint with exactly the
no-empty-object-type error. Restored the fix - clean on both, plus a
throwaway call site instantiating useTable<VerifyNoQuery,
VerifyNoQueryQuery> to prove the type satisfies useTable's `TQuery
extends object` constraint, not just that it parses in isolation.
2026-09-19 20:36:24 +08:00
zhangwenjian 8b312bed1d Merge branches 'feat/010-gen-plumbing' and 'feat/010-vue3-template' into integ/010-all 2026-09-19 15:22:03 +08:00
zhangwenjian 30bcb57f41 fix🐛: put the rules computed's comma at the end of the previous line
The rules block joined entries with a $first-flag comma the same way
defaultQuery and defaultModel do, but on its own line rather than all
on one -- so the comma for every entry but the first sat at the start
of its line instead of the end of the one before it. @stylistic/comma-style
requires the opposite, and pnpm lint fails on any table with two or
more required insert fields (one comma is enough to trip it; a table
with 0-1 required fields never renders a second entry to get it wrong).
Reproduced first: rendering a four-required-field fixture reported
three comma-style errors, matching what integration testing found on
qa010_widget and qa010_natural.

Fixed by moving the separator so it trails the previous entry instead
of leading the next one: the else branch now emits ",\n  " -- comma,
then the newline and indent -- rather than "\n  , ", so the comma lands
on the line that already has content instead of opening a fresh one.

Also fixed while reproducing, found by the same fixture: dictLabel was
imported whenever any column had a DictType, but it is only called from
the list column's dictionary branch. A table using a dictionary solely
in its insert form (no such column in the list) imported dictLabel and
never called it, tripping no-unused-vars. $hasDict now only gates
useDict; a new $hasDictList gates dictLabel specifically.

Verified against three fixtures via a throwaway go-admin-ui worktree
(deleted afterwards) with hand-written API-module stubs standing in for
F5: the regression fixture (four required fields, confirmed red before
the fix, green after), and the two fixtures from the original F4
verification round (full branch coverage, and the all-flags-off edge
case), all of which stayed green. pnpm type-check and pnpm lint both
ran clean with zero errors, on Node 24.11.0 (this machine's default
node is 20.19.0; the project's engines field wants >=22).
2026-09-19 15:21:30 +08:00
zhangwenjian 5bb211afcd fix🐛: Preview now sets tab.MLTBName before rendering, matching NOActionsGen
MLTBName (table_name with underscores turned to dashes, e.g. "user_profile"
-> "user-profile") is a gorm:"-" field - table.Get never fills it in, the
caller has to. NOActionsGen has done so since it existed; Preview never
did, so every template's import path that reads it
(from '@/api/{PackageName}/{MLTBName}' in vue.go.template, present in
both the pre-Vue3 template and F4's rewrite) rendered with the module
segment missing - "from '@/api/admin/'" - in the preview dialog only.
The real generated file was always correct.

Pre-existing, not introduced by this PRD, but worth fixing now: F3/F9
added two more Preview panes (the language packs) on top of an assumption
- that Preview's output stands in for what NOActionsGen actually writes -
that was never true for this field. Checked the rest of gen.go for the
same "only set on the write path" shape; MLTBName is the only one -
every other field Preview's templates read comes straight off the
sys_tables/sys_columns rows table.Get already loads.

Verified with a throwaway harness driving Gen.Preview through a real gin
Context and sqlite-backed db, parsing the JSON response and reading back
template/vue.go.template's import line. Confirmed red first (temporarily
removed the added line): "from '@/api/verify010/'". Restored it: "from
'@/api/verify010/verify-widget'".
2026-09-19 15:18:29 +08:00
zhangwenjian d102c4b7c1 Merge branch 'feat/010-vue3-template' into integ/010-all 2026-09-19 14:17:14 +08:00
zhangwenjian 545453c93e feat: infer column width from column_type before rendering (PRD 010 R2)
vue.go.template (F4) only had a flat fallback for an unconfigured
colWidth: 110 for datetime, 120 for everything else. R2 asks for a
precise per-type inference instead - varchar(n) tiered by n, tinyint(1)
narrower than a general integer, text/blob wide - sized so a typical
table's list columns land inside go-admin-ui's ~580px text-column
budget. text/template cannot parse "varchar(255)" itself, so this has
to run in gen.go before the template executes, not in the template.

Judgment is columnType, not goType, same as F1/F2's earlier reversal
(API契约.md §1.1): sys_tables.go:323-338 gives every non-primary-key
int/tinyint/bigint/decimal column goType "string", so goType alone
cannot tell a boolean flag from a bigint from a name column.

InferColumnWidth(columnType string) int is exported and pure per
测试用例.md §2.5's own request, so it can be pinned with an exact
input/output table rather than only asserting "the rendered page
happens not to overflow" - see column_width_test.go, including the
tinyint(1)-vs-tinyint(4) and the varchar tier-boundary cases.
applyInferredColumnWidths only touches a column already at the 0
sentinel, wired into both Preview and NOActionsGen ahead of the
template execute calls; a column already configured (by the user or F6)
is left untouched, also pinned by a test.

Verified against the real NOActionsGen call path (not just the unit
test): generated a table mixing every branch (tinyint(1), datetime,
decimal, varchar at three tiers, text, and one pre-configured column)
and printed the resulting tab.Columns[i].ColWidth after generation -
every value matched InferColumnWidth's own table, and the
pre-configured column's 333 was left untouched.
2026-09-19 14:16:37 +08:00
zhangwenjian c3d2a5952b fix🐛: getVerifyXxx's pk parameter type follows GoType, not always number
ts.go.template hardcoded the pk parameter as `: number`. vue.go.template
(F4, landed after this file) derives $pkType per table - "number" unless
the pk column's GoType is "string" (natural keys do exist; sys_tables.go
gives a pk column GoType "string" whenever its ColumnType is not
int-shaped), then types useForm on that same $pkType. A string-pk table
therefore generated a page calling get{Class}(id: string) against an api
module whose get{Class} only accepted number - a TS compile error, not
a runtime bug, and exactly the kind of gap each side's own template-only
verification could not see (F4 was checked against a hand-written .ts
stub before this template existed; this template was checked against a
numeric-pk fixture only).

Copies vue.go.template's $pkType derivation verbatim so both templates
agree by construction rather than by convention.

Verified: generated both a numeric-pk and a string-pk table's api module,
copied both into go-admin-ui and ran vue-tsc --noEmit - clean. Added a
throwaway call-site file exercising getVerifyStrPk with a string and (via
@ts-expect-error) confirming a number argument is now rejected, so the
type is actually enforced rather than having quietly widened to any.
2026-09-19 14:12:01 +08:00
zhangwenjian d65b21baf6 feat: field-level validation for sys_tables.go:357's Update (PRD 010 F10)
That bind-and-save path had zero field-level validation (API契约.md
§1.2/§2.1, decision D6), which left four ways to silently corrupt the
generator's own metadata or the language packs it writes:

  - jsonField had no format rule at all, unlike tableName/className/
    businessName, which all have a pattern check. A jsonField with a
    dot, a leading digit, or anything not matching lower camelCase
    still saved, and would land verbatim as a key segment in
    gen/{PackageName}/{BusinessName}.ts.
  - jsonField had no per-table uniqueness check. Two columns retitled
    to the same jsonField overwrite each other's generated i18n key
    with no warning.
  - businessName had no cross-table uniqueness check within a
    packageName. Two tables sharing one write the same language pack
    path and the second's generation silently clobbers the first's.
  - defaultValue is spliced into the generated defaultModel() as a
    literal, never evaluated (API契约.md §2.1) - so content shaped
    like a function call or block does not do what it looks like it
    does, and previously saved without complaint either way.

colWidth is handled differently on purpose: API契约.md §2.1 says an
out-of-range value should fall back to the inferred width rather than
be rejected, so it is reset to the 0 sentinel in place instead of
failing the request - same outcome as if it had never been set.

G10 (colliding with the built-in admin/* i18n namespace) is not one of
the four: D9 already moved generated keys into their own gen/
namespace, so that collision no longer exists.

All four checks and colWidth's sanitize-in-place path are covered by
sys_tables_validate_test.go. Confirmed red first: swapped in a no-op
stand-in for both validators and reran - 13 sub-tests that should now
be rejected or sanitized passed straight through instead (jsonField
format x5, jsonField uniqueness x1, colWidth range x2, defaultValue
expression x4, businessName uniqueness x1). Restored the real
implementation and reran - all green.
2026-09-19 14:08:05 +08:00
zhangwenjian 0f31feae6f feat: rewrite the generator's Vue template for Vue 3 (PRD 010 F4)
template/v4/vue.go.template produced Vue 2 syntax -- slot-scope, .sync,
.native -- all removed outright in Vue 3, so every generated page
failed to render (PRD 010 G1). Rewritten from scratch to match
go-admin-ui's reference page (src/views/demo/product/index.vue):
PageContainer + ProTable + useTable/useForm/useRemove,
<script setup lang="ts">.

Four constraints from the phase-3 review, all load-bearing:

- An HtmlType this template doesn't recognise (the old "file" branch,
  or anything future work adds) renders as a plain input rather than
  nothing. sys_tables.go still assigns "datetime" automatically on
  import, and a row can carry "file" from before F7 disabled it in the
  UI, so "unknown" is a real, reachable state, not a hypothetical one.
- Every column width is min-width, never width -- a rigid width
  repeats G5's overflow bug on any table with enough columns. colWidth
  (F1) backs it when set, a flat per-column-kind number otherwise.
- Not one Chinese character anywhere in the output, comments included:
  D10's CJK scan is a bare regex with no exception for "it's only a
  comment". Every label goes through
  $t('gen.{PackageName}.{BusinessName}.{JsonField}'), read from the
  language pack F9 wired into src/lang/{locale}/gen/.
- defineOptions carries the ClassName + "Manage" suffix gen.go writes
  into sys_menu.menu_name (R4) -- the old template's bare ClassName
  was a latent mismatch masked by loadView()'s runtime rewrite (G7).

Verified by rendering two fixtures through the real template.Execute --
one exercising every optional import (dictionary, foreign key,
datetime, required-field rules, search filters) and one with none of
them, a string primary key, and a select/radio with neither a
dictionary nor a foreign key configured, plus an html_type this enum
has never had. Both scan clean for CJK, and both pass pnpm type-check
and pnpm lint with zero errors against a throwaway go-admin-ui
worktree (deleted afterwards) seeded with hand-written API-module
stubs standing in for F5, which lands in template/v4/js.go.template
separately.
2026-09-19 14:04:29 +08:00
zhangwenjian 0494a27d6c fix🐛: SysColumns.Update can now clear colWidth/defaultValue back to 0/""
Updates(&e) uses GORM's struct form, which skips zero-value fields -
but 0/"" is exactly the sentinel PRD 010 F1/F2 chose for "unconfigured"
(数据库变更.md §1.1). A caller resetting colWidth or defaultValue back
to that sentinel was therefore silently ignored: the row kept its old
value, the API reported success, and reopening the edit form showed the
stale number/string again.

Not fixed by switching to Select("*") - that would also start writing
this struct's other zero-valued fields (Sort, the Pk/Required/... bools),
none of which are part of this change. A second, map-form Updates
scoped to just these two columns writes the sentinel without touching
anything else; map-form Updates does not skip zero values.

TestSysColumnsUpdateClearsSentinelFields pins the regression: seed
150/"active", update to 0/"", read back and assert it stuck. Confirmed
red before this change (got 150/"active" back) and green after.
2026-09-19 13:59:27 +08:00
zhangwenjian e2b6ddb290 feat: generate per-table zh-CN/en-US language packs (PRD 010 F3/F9)
Add lang-zh.go.template and lang-en.go.template, one key per column
keyed by JsonField, valued from ColumnComment (R3) with a fallback to
JsonField when ColumnComment is blank (G11) - both locales resolve to
the same zh-CN text today since D3 rules out machine translation and
go-admin-ui's parity test rejects blank values.

NOActionsGen writes both files to lang/{locale}/gen/{PackageName}/
{BusinessName}.ts, two levels deep so go-admin-ui's gen-namespace.ts
glob (./*/*.ts under each locale's gen/) picks them up; nesting under
PackageName (rather than a flat gen/{BusinessName}.ts) avoids two
tables in different packages silently overwriting each other's
translations, since BusinessName has no uniqueness check.

Output has to be single-quoted with no trailing comma to satisfy
go-admin-ui's eslint config, which text/template's builtin `printf "%q"`
does not produce, hence the small parseGenTemplate/singleQuote helper
shared by the two templates.

Verified end to end: generated a real table's output (including a
blank-ColumnComment column, to exercise the G11 fallback), copied the
two language packs into go-admin-ui and ran vue-tsc --noEmit and eslint
against them - both clean, comma-dangle/quotes included.
2026-09-19 13:56:34 +08:00
zhangwenjian 28eba92077 feat: generate a typed .ts API module instead of .js (PRD 010 F5)
Rewrite template/v4/js.go.template (renamed to ts.go.template) to emit
TypeScript matching go-admin-ui's src/api/demo/product.ts: an interface
per row and per query params, typed CRUD functions, and a del{Class}
that takes Id[] like useRemove expects. GoType maps to number for
int/int64/float32/float64 and string otherwise, since go-admin-ui's
request() types the {code,data,msg} envelope rather than the payload.

Preview's response map key changes from template/js.go.template to
template/api.ts.template so its Tab label matches the new content;
NOActionsGen writes the file with a .ts extension.

Verified end to end: generated a real table's output, copied it into
go-admin-ui and ran vue-tsc --noEmit and eslint against it - both clean.
2026-09-19 13:55:56 +08:00
zhangwenjian aa7a92664d feat: add sys_columns.col_width and default_value (PRD 010 F1/F2)
Back the code generator's Vue 3 template migration: col_width lets R2's
column-width inference be overridden per field, and default_value lets
R1/A6's "unconfigured rows still generate a usable page" guarantee hold
for generated forms. Both use a sentinel default (0 / "") rather than
NULL so "unconfigured" has exactly one representation - see
docs-prd/010-代码生成器前端模板迁移Vue3/数据库变更.md §1.1.

The migration and the model change land together: AddColumn reads the
column definition off tools.SysColumns's own gorm tag, so splitting them
across commits would leave one of them failing to compile.
2026-09-19 13:33:42 +08:00
wenjianzhang 68ecc5f9a8 Merge pull request #942 from go-admin-team/ci/gofmt-gate
chore🔧: gate gofmt in CI
2026-09-18 23:06:48 +08:00
zhangwenjian 8341044251 chore🔧: gate gofmt in CI
The tree is at zero unformatted files for the first time. Nothing holds it
there: gofmt drift is invisible in review because the common form of it is a
missing newline at the end of a file, which no diff reader notices and no
existing check looks at. That is how the batch cleaned up just now reached 26
files.

`make fmt-check` reports rather than rewrites. CI cannot commit, and a target
that silently reformats hides what it touched; `gofmt -l` names the files and
says nothing when there are none, so its output is both the failure message and
the fix.

The step runs before the tests rather than beside checksilent at the end. A
formatting miss is a one-command fix, and learning about it after five minutes
of tests and an end-to-end install wastes five minutes for nothing.
2026-09-18 22:57:17 +08:00
wenjianzhang 8b03400ecc Merge pull request #941 from go-admin-team/docs/agents-api-terminology
docs📝: call the layer Api in the three places that still said Handler
2026-09-18 21:15:40 +08:00
zhangwenjian 8115c3a737 docs📝: call the layer Api in the three places that still said Handler
The diagram at the top reads Router -> Api -> Service -> Model, the section is
headed "Api 层", and the directory is app/admin/apis/. Three sentences in the
body still called that layer Handler -- the name it went by before the rename,
and one nothing in the tree answers to now. A reader following the document has
no way to tell the two are the same layer.

Wording only. No rule changes, and no other file is touched.
2026-09-18 21:07:32 +08:00
wenjianzhang c01307202d Merge pull request #939 from go-admin-team/style/gofmt-tree
style💄: run gofmt over the tree
2026-09-18 21:03:46 +08:00
zhangwenjian 5ecb1e6e4c style💄: run gofmt over the tree
`gofmt -l` listed 26 files. Seventeen of them were missing the newline at the
end of the file; the rest are indentation that used spaces where the file uses
tabs, a handful of call sites written `f(a,b)`, and the doc comment spacing
gofmt has rewritten since 1.19 (`//X` to `// X`).

Nothing here changes behaviour: `go build ./...` and `go vet ./...` are clean
and `go test ./common/...` passes, which is the half of the tree these files
are concentrated in.

Only the files gofmt named are touched, so the diff reads line by line rather
than as a reflow of the whole repository. `gofmt -l` is now empty, which is the
precondition for gating it in CI -- worth doing, but a separate change.
2026-09-18 18:55:19 +08:00
wenjianzhang 9c299805c1 Merge pull request #938 from go-admin-team/fix/cache-control-stray-token
fix🐛: drop the stray token from the Cache-Control header
2026-09-18 14:53:52 +08:00
zhangwenjian 2a900c9876 fix🐛: drop the stray token from the Cache-Control header
NoCache sent `no-cache, no-store, max-age=0, must-revalidate, value`. The
trailing `, value` is not a directive; it is a leftover token that has been on
every response this middleware touches since the file was written. Unknown
directives are ignored, so nothing misbehaved because of it, but it went out on
the wire and read as a mistake to anyone looking.

The assertion added in #937 pins the old value, so it moves with the source:
removing the token from the middleware alone turns TestNoCache red, which is
the whole point of that test and the reason both lines change together here.
2026-09-18 14:47:28 +08:00
wenjianzhang 0008b943a3 Merge pull request #937 from Tuoxie423/test/header-middleware
test: add unit tests for NoCache/Options/Secure middleware
2026-09-18 14:41:31 +08:00
拖鞋423 50c74b1f96 test: add unit tests for NoCache/Options/Secure middleware 2026-09-17 23:41:21 +08:00
wenjianzhang ae1eef6d4f Merge pull request #936 from go-admin-team/fix/gen-import-accepts-json-body
fix🐛: accept the import table list from a JSON body as well
2026-09-16 20:53:03 +08:00
zhangwenjian d01cdc040f fix🐛: accept the import table list from a JSON body as well
The generator's import reads its comma-separated table list with
c.Request.FormValue("tables"), which on a request declaring itself as JSON reads
the URL query and nothing else. go-admin-ui v3.2.0 began sending that list in
the body, so the handler saw an empty string, asked information_schema for a
table named "", and every import failed with "table name cannot be empty!" —
on a fresh installation that is the first thing the generator is asked to do.

tablesToImport reads the query first and falls back to the body, so a front end
sending either works against this server. It also drops blank entries:
splitting "" yields one empty name rather than nothing at all, which is why the
old code reached a database query at all before failing.

The front end sends the list in the query again on its side; this half is what
lets an installation already running v3.2.0 recover without changing it.
2026-09-16 17:57:51 +08:00
zhangwenjian 92b9af17b7 refactor🎨: name the empty-table-name message once
The string was spelled out at each site that raises it, and once more in the
test file that asserts on it. A test holding its own copy cannot tell the
difference between the handler answering something else and the message having
been reworded: it goes on asserting a string the server no longer sends, and
goes on passing.

The three copies in app/other/models/tools are left alone; they are raised from
a different layer and nothing asserts on them.
2026-09-16 17:57:37 +08:00
zhangwenjian 898e1b023a refactor🎨: share the generator tests' engine and response decoding
newEngine takes the method, path and handler, so a second test file does not
have to restate the sqlite connection, the driver override and its cleanup, the
CustomError middleware and the two context keys. serveJSON does the same for
running one request and decoding the envelope.

Nothing about what is asserted changes; newColumnListEngine and columnListMsg
keep their names and their callers.
2026-09-16 17:57:28 +08:00
zhangwenjian 1cd39b80b3 test: fail when Remove parks a goroutine on an abandoned channel
#924 buffered the channel Remove returns, so the goroutine it starts can
finish when the caller has already timed out and walked away. That half of
the patch was not covered: reverting the buffer on its own left the whole
suite green, while the timeout assertion it shipped with stayed red only for
the error-return half.

This test abandons twenty channels and then requires every goroutine to be
gone. It waits for the scheduler to empty first, so a goroutine that has not
yet reached its send cannot make the test pass while proving nothing.

Without the buffer: 20 of 20 parked on `chan send` at jobbase.go:216.
2026-09-16 08:27:24 +08:00
wenjianzhang a90c67473e Merge pull request #924 from jackwalkerlabs/fix/job-stop-timeout-890
fix🐛: report job stop timeouts as errors
2026-09-16 08:02:47 +08:00
wenjianzhang 1a84b8a892 Merge pull request #930 from Tuoxie423/docs/remove-dead-contributor-links
docs📝: 删除 README 中失效的贡献者链接
2026-09-16 08:02:43 +08:00
wenjianzhang 656d14cd54 Merge pull request #934 from go-admin-team/fix/928-929-seed-repair
Reseeding a menu tree repairs what it finds, and finds what it wrote
2026-09-14 14:52:33 +08:00
zhangwenjian 9dd271ecab fix🐛: claim the rows an application wrote before seed_code existed
1786700008000 added sys_menu.seed_code and left it NULL on every row that was
already there. That is right for the host's own hand-placed menus: there is
nothing to derive one from.

An application's rows are in that population too, and for those it is
derivable - menu_name is what identified them before the column existed. The
natural-key lookup missed them, so a reseed inserted a second copy beside each
one, and the new unique index could not object, because NULL never collides on
MySQL, PostgreSQL or SQLite and is filtered out of the index on SQL Server.

Claimed when the application is seeded rather than by a backfill migration.
The value is only derivable where the spec's own Code is in hand: menuName
concatenates two pascalCase strings and does not reverse, so a migration
looking at menu_name alone would be guessing. For the same reason more than
one match is refused and named rather than picked from - attaching an
application's menu to whichever row the database returned first is the failure
this is meant to prevent, not a smaller version of it.

The match is scoped to the application's own app_code, so a row belonging to
another application, or to the host, is not claimed.

An adopted row then goes through the ordinary repair, so it comes out carrying
what the spec says rather than what it held from before.

Three degradations turn the new assertions red: not adopting at all, picking a
row when there is more than one, and dropping the app_code from the match.
2026-09-13 20:34:04 +08:00
zhangwenjian 4973ee030d fix🐛: bring an existing seeded menu up to what the spec says
repairExistingMenu reconciled a row's paths and its api bindings and left
every other column as an earlier run had written it. That cost three different
things, and none of them announced itself.

A menu whose parent was removed and seeded again kept parent_id pointing at
the dead row while its paths named the new one. The tree is built from
parent_id - SysMenu.GetPage walks down from ParentId == 0 - so the menu was
gone from the sidebar, with the migration reporting success.

A menu somebody added by hand under a seeded one kept the old prefix when its
ancestor moved. It is in no spec, so nothing else would ever rewrite it;
SysMenu.Update already does this cascade for the same column when a menu is
moved through the UI.

An application that renamed a menu, or moved its component, in a new version
had the change ignored: the row was found by its natural key and returned
untouched.

The row a spec describes now has one definition, and both the insert and the
repair use it - they cannot drift into disagreeing about what a spec decides.
The repair writes every column on that list.

Visible and IsFrame are deliberately not on it. They are seeding defaults the
application never expressed, so an administrator who hid a seeded menu keeps
it hidden; there is a test that hides one and reseeds.

The cascade matches the row itself or a row strictly underneath it, rather
than `paths LIKE old || '%'`, which also catches /0/1/20 when old is /0/1/2.
An empty old path takes the single-row branch instead: there is no subtree
under one, and the LIKE would have matched the whole table.

Five degradations turn the new assertions red: not writing the spec columns at
all, leaving ParentId off the list, putting Visible on it, not cascading, and
cascading on the loose prefix. The last one did not, at first - the decoy rows
were built against the path of the menu whose parent moved rather than the
path that actually gets rewritten, so the prefix they collided with was never
the one passed to the query.
2026-09-13 20:33:47 +08:00
wenjianzhang 137bb3ad33 Merge pull request #931 from go-admin-team/feat/008-wire-example
008 layer three: status reads sys_app, dependencies are checked, and the example gets installed for real
2026-09-12 21:17:33 +08:00
zhangwenjian 03d587db6a ci👷: give the end-to-end install a make target
It was an inlined `go test` in the workflow, and the only thing in the build
that runs it. Every other gate there goes through make - make test, make
build, make checksilent - and `make test` is `go test ./...` in this module,
which cannot reach test/e2e-apporder because that is a module of its own.

So the one check that exercises installing an application was the one check a
developer had no command for, and the only place it could turn red was after
pushing.
2026-09-11 08:23:42 +08:00
zhangwenjian 1f56b956d2 test: build the end-to-end binary once, and assert from one list
Three tests each called newEnv, and newEnv built the binary, so the same
binary was linked three times - about 17 seconds of the run, measured. A
binary is read-only and there is nothing to isolate between tests; each test
still gets its own directory and its own database. The package now builds it
on the first test that needs one and removes it in TestMain. The suite goes
from 39 seconds to 9.

The three assertion blocks listed the same six queries, two or three times
each, differing only in the counts expected - so renaming a table meant
finding three places. They now share one list, with a flag for the uninstall's
"all of them at zero". The reinstall check gets stronger on the way past: it
was three of the six and is now all six.

The per-call sql.Open in count and exec stays. It looks like waste and is not:
the binary under test writes the same file, and a connection held open across
a run of it is a second writer for nothing. The shared part is factored out;
the opening is still per call, and the comment now says why.
2026-09-11 08:21:36 +08:00
zhangwenjian 37aece9791 test: share one sys_app fixture, and count through the helper that checks
Two tests in this package each wrote out what an installed sys_app row looks
like, field for field, and a third inlined the same Create with a different
status. One appRow(t, db, code, status) now covers all three, so a new NOT
NULL column on SysApp is one edit rather than three.

One assertion counted with a bare db.Model(...).Count(&n) and dropped the
error that call returns. A failing query leaves n at zero, which is exactly
what that assertion wanted to see - so the test would have passed on a broken
query. The package already had a count helper that fails on the error, and
this now uses it.

Also a cycle reached from outside itself. The existing case walks straight
into its own cycle from the first code, so the path trimming had nothing to do
and replacing it with the untrimmed path left the test green - the trimming
was never covered. With a requiring b, b requiring c and c requiring b, the
untrimmed report names a as part of a cycle it is not in, and the test goes
red.
2026-09-11 08:21:35 +08:00
zhangwenjian 309b400bc0 refactor♻️: take one registry snapshot, and one route into sys_app
Cleanup from a review pass over this branch. No behaviour changes except the
two noted below.

runInstall took app.Snapshot() twice, once inside manifestFor and once for the
cycle check. Snapshot is a deep copy of the registry, and worse than the
copying, the two calls could in principle disagree - the set the cycle check
validated was not provably the set the manifest came from. One snapshot,
passed to both.

appSummary converted a display code back to a stored one with
NormalizeAppCode, which is not that inverse: it leaves "core" as "core", so
the framework needed a branch of its own to stay out of the listing. AppFilter
is the documented inverse and maps it to the empty string, which is not a code
any row is filed under - so the branch goes, and the function now matches
filterAppsByApp twenty lines below it, which was already using AppFilter.

That branch only half-covered what it guarded: a sys_app row carrying an empty
or reserved app_code was still merged into the framework's group by
groupByApp, with only its summary suppressed. loadApps now drops such rows,
which is the one place that settles it for every reader of the map.

requiresInstalled built two parallel slices with a tuple assignment repeated in
three branches; it now picks a reason and appends once. Its last arm was a
catch-all on "not installed", so a status constant added later would have been
described as "did not finish" - a sentence that would be wrong for whatever
reason the constant was added. Unrecognised values now say so. It also takes
the normalised code the caller already has rather than computing it a third
time.

refuseOnDependencyCycle sorted each manifest's Requires before walking them.
Requires is a slice and already has a fixed order, so the sort bought no
determinism - that comes from the sorted outer loop, which walks a map - and
only made a reported cycle harder to line up against the manifest that caused
it. The filtering pass that went with it is covered by the registration check
underneath. The cycle path is trimmed with slices.Index, which also removes a
fallback return that the grey/path invariant made unreachable.
2026-09-11 08:20:56 +08:00
zhangwenjian 1b9868b72b feat: refuse an install whose dependencies are not installed
An application's manifest can name others it needs. Until now the list was
stored and never read.

It is checked, not satisfied. Installing the dependencies too would make
"install this application" mean "and everything it happens to name, and
everything those name" - a blast radius the operator did not ask for and
cannot see beforehand. What they get is the list and the order to do it in.

A dependency whose own install failed, or never finished, is not a dependency
that is there. The message says which, because the two send you to different
places: one to install it, the other to look at why it did not take.

The check runs before anything is written, so a refusal cannot cost the
operator the row that told them what they had.

Separately, a cycle anywhere in the registered manifests is refused, whether
or not the application being installed is in it. A cycle between two others is
still an authoring mistake, and the day somebody installs into it - with an
error naming two applications they did not ask for - is the worse time to find
out. The error is the cycle rather than the walk that reached it, and the
walk's order is sorted, so the same set of manifests always reports the same
one. Requires naming an application that is not registered is not a cycle; it
is the database's answer to give, at the time it matters.

Six degradations turn the new assertions red: accepting any dependency,
accepting a row regardless of its status, returning no cycle, not trimming the
reported path to the cycle itself, and running the check after the row has
already been written - the last of which was rebuilt after the first attempt
at it deleted the check rather than moving it, and so went red on the wrong
assertion.
2026-09-10 21:30:29 +08:00
zhangwenjian 25344aa572 feat: let migrate status say what sys_app knows
The migration rows answer "did this run". They cannot answer "is this
application installed", and the difference is not academic: an install that
stopped partway leaves every migration reading applied and a row saying the
install never finished. Until now nothing printed that row.

    [order]  1.0.0 failed at order-1793800000000
      applied   order-1793800000000  2026-09-10 21:02:07

The application list is the union of the two sources rather than either one.
Reading it from sys_app alone would drop an application whose migrations ran
under plain `migrate`, which records no row; reading it from the migration
rows alone drops one whose code has been taken out of the binary, which is
when somebody most wants to see it named - that one now gets a group of its
own, empty, saying why.

A database from before sys_app existed prints exactly what it printed before.
`migrate status` has to keep working on a database that has not been migrated
at all, which is when it is most wanted.

Four degradations turn the new assertions red: dropping the sys_app-only
applications from the listing, printing no summary, not narrowing sys_app by
--app, and reporting an unfinished install as an installed one.
2026-09-10 21:30:03 +08:00
拖鞋423 ea9d27cf6d docs📝: 删除 README 中失效的贡献者链接 2026-09-09 20:28:35 +08:00
zhangwenjian 0bee8ec46c ci👷: run the end-to-end install on every push
The separate module is invisible to `go test ./...` in the main module, which
is the point, and also means nothing would ever run it. This step does.
2026-09-09 16:40:15 +08:00
zhangwenjian 3a5afeb518 test: install and uninstall the example application end to end
Everything under `migrate install` was covered with an injected engine and a
hand-built schema, which is where the shapes belong. What none of it could
catch is the wiring: whether an application's init() reaches both registries,
whether the installer finds a manifest through app.Snapshot, whether the
seeder writes what the uninstaller goes looking for, and whether the command
exits non-zero when a migration fails - which a deployment reads to decide
whether to start the new version.

This builds a go-admin binary with the example application linked in, migrates
a real database with it, and drives the whole sequence: framework migrations
only, install, install again, put a row in the application's own table,
uninstall, reinstall.

It lives in its own module. A tagged import in the main module would still be
resolved by `go mod tidy`, which considers every build tag and would go
looking for github.com/go-admin-team/example-app-order on the network - a
repository that does not exist, because the example is a directory inside this
one. That was checked rather than assumed: tidy fails there with "Repository
not found". A build tag of `ignore` is skipped by tidy but cannot be turned on
either, because the standard library uses it for files that are not meant to
build at all. A separate module with replace directives is invisible to the
main module's tidy, its build, its tests and checksilent, and needs no
go.work.

Three degradations turn it red: the example application not registering a
manifest, the uninstall not clearing sys_migration - where the reinstall then
seeds nothing and the assertion reads "menus = 0, want 4" - and the seeder not
recording its grants, where the uninstall then leaves every policy behind.
2026-09-09 16:40:15 +08:00
zhangwenjian fc8ba4d615 feat: let the example application say what it is
app-order registered its migrations and its menus and nothing else, so
`migrate install order` answered that no application in the binary registers a
manifest. Which was true, and made the installer untestable against the one
application this repository ships.

The manifest goes in the migration package rather than one of its own because
that is the package a host has to import for the application to exist at all -
its migrations register from there too. A second package would be a second
thing to remember to import, and forgetting it would leave an application
whose migrations run and which no installer can name.

Its Version is not the migration version and the two move independently:
adding a migration file without renaming the application is normal, and so is
a release that changes no schema. The migration versions decide what runs;
this decides what the installed row says.

go-admin-core moves to v2.8.0, which is where contract/app lives.
2026-09-09 16:40:14 +08:00
wenjianzhang dc20062e5b Merge pull request #927 from go-admin-team/fix/sqlserver-null-unique
The seed natural-key indexes cannot be built on SQL Server
2026-09-09 16:19:26 +08:00
zhangwenjian ff430c509b ci👷: run the migration tests against SQL Server too
The fourth registered driver, and the one that disagrees with the other three
about NULL. A suite that never pointed at it reported success for a migration
no SQL Server database could apply - the same shape as the PostgreSQL gap that
put the postgres service here, one driver further along.

SQL Server has no equivalent of POSTGRES_DB, so the database the DSN names is
created in a step before the tests. The test helper fails rather than skips
when CI is set and the variable is not, so dropping the service or renaming
the variable cannot quietly go green.
2026-09-09 13:49:20 +08:00
zhangwenjian d43d7a46dd fix🐛: make the seed natural-key indexes buildable on SQL Server
1786700008000 could not be applied to any SQL Server database. Not an old one
with awkward data - any of them, including an empty one:

    Msg 1505 ... duplicate key ... The duplicate key value is (, <NULL>, 0).

MySQL, PostgreSQL and SQLite treat two NULLs in a unique index as different
values, so any number of rows missing a seed_code coexist under
uk_sys_menu_app_seed_code_del. SQL Server treats them as equal and permits
exactly one. 1786700001000 seeds five menus and none of them has a seed_code,
so the second one already collides with the first. sys_api's index has the
same shape over two nullable columns, path and action.

On SQL Server the index is now filtered to the rows that carry a value, which
is what the other three engines do by not comparing their NULLs. The filter is
not added elsewhere: MySQL has no filtered index at all, and on PostgreSQL and
SQLite it would only restate what those engines already do.

Nothing that has applied this migration is affected, and no SQL Server
database can have.

Verified against SQL Server 2022. The migration completes; the filtered index
still rejects a second (order, dir) and still lets another app reuse "dir",
so filtering removed the NULL rows from the index rather than the index's
teeth. Two degradations turn that red: dropping the filter, and naming only
path in sys_api's - the second one needed a fixture row with a path and no
action, because rows missing both are excluded either way and the first
attempt at that degradation came out green.

There is also a control test asserting the unfiltered statement still fails on
this engine, so the first test is passing because of the fix rather than
because SQL Server turned out not to mind.
2026-09-09 13:49:20 +08:00
wenjianzhang 72c496ab93 Merge pull request #926 from go-admin-team/feat/008-installer
008 layer two: the installer and the uninstaller
2026-09-09 13:43:08 +08:00
zhangwenjian b228152308 docs📝: drop a reference to a document this repository does not carry
Two comments added in this branch cite docs-prd/008-.../数据库变更.md by path.
That directory is not tracked here, so the citation reads as a file the reader
can open and cannot. The reasoning it pointed at is short enough to state in
place.

Three comments from the previous batch cite the same path and are left alone;
they belong to a different change.
2026-09-09 13:28:45 +08:00
zhangwenjian 006756ea40 test: cover the uninstall's chunk boundary and its empty id lists
Copilot could not review this branch - the account is over its review quota -
so these are what a second pass over the uninstaller turned up. No defect: the
three cases were uncovered rather than wrong.

findOrphanPolicies batches its OR chain because a driver runs out of
placeholders long before an application runs out of endpoints, and nothing
exercised the boundary. 205 paths across three batches, the last one short,
plus one policy no key names as a control. Taking one fewer per batch,
advancing one too far, and stopping after the first batch each turn it red.

An application with apis and no menus, and one with menus and no apis, are
both normal - endpoints another service calls, or a section with no endpoints
of its own - and each leaves one of the two id lists the uninstall reads
empty.

That last pair also corrected a comment. The guard in front of the join-table
delete was described as being there because an empty IN list is a syntax
error. It is in raw SQL, but GORM renders IN with an empty slice as a
condition that matches nothing, and removing the guard leaves the new test
green. It stays as a statement of intent, and now says so.
2026-09-09 12:56:33 +08:00
zhangwenjian aa539c061f feat: uninstall one application's menus, apis and grants
`migrate uninstall <code>` removes what an application's install wrote and
leaves the application's own tables alone. Removing an order module is not the
same decision as destroying the orders, and nothing here can tell an operator
who is done with it from one who will reinstall tomorrow.

One transaction, and this one really is one: every statement is DML or a
SELECT, so unlike an install there is no DDL to commit it out from under
itself. Child rows go first, while the ids that identify them can still be
read from their parents, and the api paths are read before the rows carrying
them are deleted.

The two join tables need no ledger and get none. menu_id is a surrogate key,
so a sys_role_menu or sys_menu_api_rule row can only have come from a menu
this application wrote - there is no "looks like it but is not". A column on
sys_role_menu would have been worse than unnecessary: SysRole.Update deletes a
role's rows and writes them back through GORM's many2many, which does not
carry extra columns, so the column would be blanked the first time anybody
edited a role, silently. There is a test that performs that edit and then
uninstalls.

casbin_rule is the opposite case, because its key is business text somebody
else may have written for their own reasons. Policies are removed one at a
time, by exact tuple, and only the ones the ledger says this install created.
A tuple the ledger names that is no longer there is reported, not treated as a
failure - the uninstall wanted it gone and it is gone. Then, with everything
the ledger could speak for already dealt with, a read-only pass lists the
policies still naming this application's paths: those are grants somebody made
by hand, they are about to point at APIs that no longer exist, and they are
not this command's to delete. The two lists stay separate because they mean
different things - one is something of ours that had already gone, the other
is somebody else's grant now pointing at nothing - and merged into one "could
not remove" list neither would be actionable.

sys_migration's rows for the application go too. Without that a reinstall
finds every version applied, runs no migration, seeds nothing, and reports
success. It is the easiest step to leave out, because a migration record does
not look like the application's data.

A sys_app row is not required. `migrate` with no subcommand applies every
registered migration, an application's included, so an application can have
all of its rows without ever having gone through the installer - and that is
the case where nothing else can clean up after it.

Eleven degradations were applied one at a time, each red on the assertion it
was aimed at: skipping either join table, deleting sys_role_menu without its
filter, skipping sys_migration, deleting sys_migration without its filter,
matching policies by path instead of by ledger tuple, dropping the orphan
pass, treating a missing policy as a failure, leaving the ledger behind, soft
deleting sys_menu instead of removing it, and running the whole thing outside
a transaction.
2026-09-09 12:35:35 +08:00
zhangwenjian 74b0ee8776 fix🐛: stop sys_menu declaring an index stricter than the real one
uk_sys_menu_app_seed_code_del covers (app_code, seed_code, deleted_at) and is
created by 1786700008000 with explicit SQL. The struct tag named the same
index on SeedCode alone, and a named uniqueIndex tag collects only the fields
carrying that name - so AutoMigrate on this model would build a unique index
on seed_code by itself: stricter than the real one, and forbidding two
applications from both having a "dir" node, which the composite key exists to
allow.

Worse than being stricter, it would win. The migration only creates its index
when HasIndex says the name is free, so a schema built by AutoMigrate first
keeps the wrong index and the migration steps over it without a word.

The tag cannot express the real index: deleted_at comes from the ModelTime
embed shared by every table, which no single model can add a tag to. So the
tag goes and the migration is the only thing that creates it.

No database is affected. The initial table migration AutoMigrates a frozen
snapshot of this model that has neither app_code nor seed_code, and nothing
else in the repository AutoMigrates the live one - which is why this stayed
invisible until a test built the schema from the live model and seeded two
applications, and got a unique-constraint failure on a seed code they are
supposed to be able to share.
2026-09-09 12:35:17 +08:00
zhangwenjian 412413c12f feat: record which casbin policies an install created
sys_app_casbin_grant has existed since the registry tables were added and
nothing ever wrote to it. An uninstaller reading it would have found it empty,
deleted no policy at all, and reported every one of them as an unattributable
leftover - which is what "report and skip" looks like when the ledger was
simply never written, and is indistinguishable from it working.

grantToAdminRole now writes an entry for each policy it creates. The entry
carries the tuple casbin_rule is unique on rather than a foreign key into it,
because casbin_rule is not this project's table: the gorm adapter's SavePolicy
truncates it and writes it back from memory, and SysRole.Update replaces a
role's policy rows wholesale. Both rebuild the same tuple from the same
sys_menu/sys_api data, so a match on the tuple survives what a row id does
not.

Only policies this install actually created are recorded - the insert is
conditional and its RowsAffected says which. A policy that was already there
was granted by somebody else and is not this app's to take away.

The two ways that can be wrong are not equally bad, which is what settles it.
Under-recording leaves a policy behind and the uninstall says so, because a
policy naming this app's own path with no ledger entry is exactly what it
reports as an orphan. Over-recording deletes somebody's authorization,
silently. Between a visible leftover and an invisible deletion, take the
leftover.

The ledger insert is itself conditional, for a case the obvious retry test
does not reach: on a plain re-run the policy still exists, so the insert is
skipped before the ledger is touched. It is reached when the policy row was
removed while its entry stayed, and a plain insert would then abort the whole
seed on the ledger's unique index. There is a test for that specific shape,
and replacing the insert with a plain one turns it red - which the plain retry
test does not.

Ordering: the ledger table is created by a framework migration, and version
strings sort bare digits ahead of any app-prefixed one, so it exists before
any application's seed runs. Nothing in the framework's own migrations calls
SeedMenus.
2026-09-09 12:27:38 +08:00
zhangwenjian 35d213f339 feat: install one application from its manifest
`migrate install <code>` brings one application up to the version its manifest
declares: it runs that application's outstanding migrations and records what
it did in sys_app.

It goes under migrate rather than under the existing `app` command, which
already means "generate the skeleton of a new application" - a directory that
does not exist yet, not an application already compiled into this binary.
Installing one is running its migrations, which is what this command is, so
--domain, resolveDB and the guard that refuses a mistyped code instead of
reporting a successful no-op are all already here.

Three phases, each committing on its own, and they are not one transaction.
An application's versions are separate migration files, and on MySQL a DDL
statement commits the transaction around it - destroying an outer transaction
and every savepoint taken from it. So this does not promise that a
half-installed application cannot happen. It promises one is visible when it
does: phase A writes "installing" before anything that can fail, phase B runs
the migrations, phase C turns that into "installed" or into "failed" with the
version it stopped on.

What is left to apply comes from sys_migration, never from sys_app. sys_app
is a derived view - a summary, and the answer to "which version does this app
think it is at". If it were the authority, an operator who deleted
sys_migration rows by hand would be told an application is installed while its
schema is not, which is worse than not knowing. So "already installed, nothing
to do" needs all three: nothing outstanding, recorded as installed, and the
same version. A row stuck at "installing" - what it reads as after the process
was killed partway - is not installed, and retrying is just running the
command again.

An upgrade is in place and keeps the first install's time; a downgrade is
refused, and refused before phase A writes anything, so a refusal cannot cost
the operator the row that told them what they had. An unparseable recorded
version is refused the same way, while it is still readable.

The report ends by saying the code is not running yet. That is not a
pleasantry: Go links at build time and Vite resolves its import globs at build
time, so installing an application writes its menus, its APIs and its
permissions and cannot make one line of its code run - and the menus appearing
is exactly what makes an operator believe otherwise.

Ten degradations were applied one at a time to check the tests name the
behaviour rather than the shape: deciding the no-op from sys_app alone,
always writing installed_at, allowing the downgrade, keeping the previous
attempt's diagnostics on a row that now says installed, treating "installing"
as installed, truncating last_error by bytes so a Chinese message is cut
mid-rune, not recording the failure at all, skipping code normalization, and
writing phase A before either the downgrade or the version-parse refusal.
Each went red on the assertion it was aimed at. An eleventh was discarded
rather than counted: it failed in the first install's setup, not on the claim.
2026-09-09 12:22:16 +08:00
zhangwenjian 9c68bc25a5 refactor♻️: report a failed migration instead of ending the process
run() called log.Fatalf on the first migration that failed, which ended the
process from inside the migration engine. Nothing above it could record what
happened - an installer needs to write down which version an attempt stopped
on - and no test could exercise a failing migration at all without taking the
test binary with it, which is why the one test that covers a failed migration
drove the registered function directly and left the scheduler uncovered.

run(), Migrate() and MigrateApp() now return an error, and the exit moved to
the command layer where the exit code is the command's business.

Two of those errors say more than "it failed". A migration that fails comes
back as a *VersionFailure naming the version, because an installer records
that as a diagnostic snapshot - the authoritative answer to where a retry
resumes is always recomputed from sys_migration, never read back, and asking
the database what is still pending answers a different question that merely
has the same answer most of the time. An app code nothing registered under is
now an error rather than a log line, so an installer asking for one app by
name cannot be told that installing an app that does not exist succeeded; the
command layer still rejects a typo before any database work.

exitOnError is what makes the command exit non-zero, and it covers more than
it replaces. Every path out of migrateModel used to return without an exit
code: an unreachable tenant database or a failed AutoMigrate printed a line
and exited 0, so a caller that migrates before starting a server - the deploy
workflow does exactly that - carried on onto a schema that had not been
brought forward. A failing migration function was the only failure reported,
and only as a side effect of the log.Fatalf this commit removes.

Each of these was checked by degrading it and watching the named assertion
go red: returning nil instead of the failure, naming the first version rather
than the one that failed, accepting an unregistered app code, and not exiting.

One gap is left open deliberately. Go allows a call whose only result is an
error to stand as a statement, so `migration.Migrate.Migrate()` still compiles
while dropping what it returns - `go build` passed while migrateModel was
doing exactly that during this change. Both call sites now return the value,
which the compiler does check, but nothing guards against the statement form
coming back. A checksilent rule was considered and dropped: that tool parses
without type information, so it could only match the method name, and a guard
that fires on any type with a Migrate method is noise.
2026-09-09 12:14:12 +08:00
wenjianzhang 9c5d9d16a7 Merge pull request #925 from go-admin-team/fix/deploy-image-bloat
Stop the demo host filling up with old images
2026-09-09 10:54:08 +08:00
zhangwenjian 46c10f999a ci👷: reclaim before the pull, not only after a healthy deploy
Cleaning up after a successful deployment never runs on the host that needs it.
The pull is the first thing in this script that needs space and it is where a
full disk stops it, so the run ends before reaching any cleanup - and so does
the next run, and the one after that. That is not hypothetical: a deployment
failed on the pull with no space left on the device, and rerunning the workflow
unchanged failed at the same place. The disk had to be cleared by hand before a
deployment could go through.

The pipeline is now a function called twice, before the pull and after the
health check, so the window is bounded on both sides.

Verified against a real docker daemon, in the function form rather than the
inlined one: with five images newer than the running one, so position alone no
longer protects it, it leaves three and does not select the live one; removing
the id exclusion from the same function does select it. With NAME pointing at a
container that does not exist it selects nothing - as it also does without the
explicit guard, which is there because grep -v on an empty id reads like the
opposite of what it does, not because it changes the outcome.
2026-09-09 08:11:26 +08:00
zhangwenjian d12f40c9a0 ci👷: remove this repository's old images after a healthy deploy
Every deployment pulls an image tagged with its commit and nothing removed the
previous one, so they only accumulated. 68 had built up when a deployment failed
on a pull with no space left on the device. That is the harmless place to fail -
the site kept serving the image it already had - but no later run would have
recovered on its own.

Three are kept so a release can be re-run by tag by hand. Only this repository's
images are listed, because the host runs other services. The image the new
container is on is excluded by id rather than by position, and rmi is called
without -f so an image a container still holds is refused rather than taken from
it.

Verified against a real docker daemon: with five images newer than the running
one, so position alone no longer protects it, the pipeline leaves three and does
not select the live one. Removing the id exclusion from the same pipeline does
select it, so that guard is load-bearing rather than decorative.
2026-09-09 07:45:51 +08:00
zhangwenjian cd363fce3d perf👌: stop shipping a C toolchain in the runtime image
gcc and g++ were 273MB of a 381MB image, and nothing in the container ever
invoked them: the binary is compiled and statically linked before the image is
built and arrives as a COPY, and Go is not installed here either.

The layer cost more than its size. apk resolves against an index that moves, so
its digest differed on every build and no two images shared it - a host that
keeps one image per deployed commit paid the full 273MB each time rather than
storing it once.

libc6-compat is kept although nothing measured needs it: a container built
without it resolves a hostname and opens a database connection exactly as one
built with it, but it costs half a megabyte and covers a ./main that was linked
dynamically, which this Dockerfile cannot check.

Verified by building this Dockerfile and running the result under the check the
deploy script uses - captcha answering 200 and the log reporting the datastore
connected. A control built from the current recipe passes the same check and
carries a 273MB apk layer this one does not; a third build with a deliberately
truncated binary fails the check, so it distinguishes a serving process from a
dead one.
2026-09-09 07:45:40 +08:00
Jack Walker 709cebd4a7 fix🐛: return an error when stopping a job times out
Fixes #890
2026-09-08 19:26:49 -04:00
wenjianzhang ba5ef9f79c Merge pull request #923 from go-admin-team/feat/008-host-schema
008: the application registry, its natural keys, and an idempotent seed
2026-09-08 20:35:28 +08:00
zhangwenjian 2c50317a98 fix🐛: stop the duplicate check refusing what the index would accept
The check grouped by (app_code, path, action) and refused whatever
appeared more than once. GROUP BY treats two NULLs as the same value; a
unique index treats them as different ones and allows both. So a
database holding rows with a null path or action was refused for
duplicates the index it was blocking would have accepted - and the
migration stopped, on a database with nothing wrong with it.

Both columns are nullable: neither carries a not-null tag, so gorm built
them that way. Measured on MySQL 8.0, PostgreSQL 15 and SQLite: two rows
with both columns null are one group to GROUP BY, and the unique index
builds over them without complaint. Standard SQL, not a dialect quirk.

They are now excluded from the check rather than grouped. Each column
needs its own exclusion and has its own test: one null column is enough
to make the index accept the pair, so removing either condition alone
lets that half through - which is what the two subtests are for, and
each fails only for its own half.

The message could not name the rows either. MySQL's CONCAT returns NULL
when any argument is, and scanning that into a string fails with
"converting NULL to string is unsupported" - so the check reported a
driver error instead of the duplicates it exists to report. SQLite and
PostgreSQL treat a null argument as empty and say nothing, which is why
this never surfaced in the tests: they run on SQLite, and this
repository has no MySQL in CI. The comment says so, so that a
postgres-only test file is not mistaken for cover.

No COALESCE was added to paper over that. It would have had nothing left
to guard once the nulls are excluded, and it would make a future
regression quieter: someone dropping the exclusions would get a report
naming rows that are not duplicates, which reads as a real answer,
rather than a scan error that reads as a broken query.

Leaving path and action nullable is deliberate. Tightening them is a
migration of its own - existing null rows have to be given values, and
what those values should be belongs to whoever owns the data, not to a
migration whose job is adding an index.
2026-09-08 20:17:50 +08:00
zhangwenjian 28350a15bb fix🐛: repair the row a retry reuses instead of walking past it
The idempotency check added in the previous commit stopped a retry
inserting a second copy, and introduced a quieter failure in its place:
a retry that found an existing sys_menu row skipped everything after the
insert. Those are the sys_menu_api_rule bindings and the materialized
path, and neither is written by the statement that writes the menu -
paths is a separate UPDATE, and on MySQL an earlier DDL has already
committed the transaction that was supposed to hold them together.

So an install interrupted between those steps left a menu that exists,
sits outside the tree with an empty path, and is bound to no API. What
core's contract.md says about such a menu is that it is invisible to
every role and its apis are authorized for no one - while the installer
reports success.

Reusing now repairs. paths is compared before it is written, so a row
that is already right is not touched. Bindings are inserted with WHERE
NOT EXISTS rather than deleted and rebuilt: an administrator can bind an
api to a menu from the menu screen, and delete-then-rebuild would take
that with it on the next retry - the same accident as sys_role.go's
Association.Delete, pointing the other way.

Confirmed as a defect before it was fixed, by building the half-written
state and watching the assertions fail:

    dir.Paths = "", want "/0/1"
    binding count for list = 0, want 1

Four paths through the repair, each with a degradation that reds its own
test and leaves the others green: missing bindings only, missing paths
only, both, and neither. The fourth asserts no UPDATE is issued for a
row already correct.

A fifth covers what the repair must not do. Rebuilding bindings instead
of inserting them leaves every other test green while silently deleting
a binding this code did not create; that one now fails with "a retry
silently deleted a binding it does not own".

Bindings an older version of a manifest created and a newer one no
longer lists are left alone. Removing them is a delete, and a delete
needs the same certainty about ownership that uninstall does - this
function cannot tell a stale binding from one somebody added by hand.
2026-09-08 19:51:22 +08:00
zhangwenjian c6d3ea5f81 fix🐛: stop a retried seed from inserting a second copy
seedApis and seedMenuTree were bare tx.Create calls. A migration that
failed partway and was run again re-inserted everything it had already
written - which is not hypothetical: the demo site collected eighteen
duplicate sys_menu rows this way, and three duplicate menus were visible
in its sidebar.

Both now look for a live row already holding the natural key and reuse
it. Only live rows count: a row an earlier soft-delete retired does not
stand in the way of a fresh insert under the same key, which is also
what the unique indexes allow.

The app_code half of each key has a test of its own. Without it the
lookups still passed every existing test while quietly letting one
application adopt another's rows - and an uninstall would then delete
rows the other application believed were its own, on both sides without
an error. Removing app_code from either lookup now fails with
"has 1 row(s) ... want 2 - one per app".
2026-09-08 19:26:42 +08:00
zhangwenjian 7fadb4b585 feat: give the seeded rows a natural key to be found by
Seeding needs something to look for before it inserts, or a retry writes
a second copy of everything it already wrote. sys_api already had one in
(app_code, path, action). sys_menu had nothing usable: menu_name is
pascalCase(appCode) + pascalCase(code), which is not injective -
"list-all", "listAll" and "list_all" all become "ListAll" - so the
original code cannot be recovered from it. Hence a new column.

seed_code is nullable, against this repository's habit of NOT NULL
DEFAULT '' for a new column, and deliberately. Every row that predates
it has no meaningful value, and under a unique index an empty string
collides with every other empty string while NULL collides with nothing.
The convention exists because deleted_at's nullability broke a unique
index; here nullability is what makes one possible.

The unique index on sys_api cannot simply be created: a live database is
known to hold historical duplicates - the demo site had eighteen. The
migration looks first and refuses while naming the offending rows,
rather than letting CREATE UNIQUE INDEX fail with a constraint error
that names none. Same shape as 1786700003000's refuseOnDuplicates.

Run against SQLite, MySQL 8.0 and PostgreSQL 15, including the CONCAT
duplicate check, which had only ever been executed by SQLite's driver.
2026-09-08 19:26:42 +08:00
zhangwenjian 691df82016 feat: add the sys_app registry and the casbin grant ledger
sys_app is one row per installed application. It is physically deleted
on uninstall rather than following the millisecond soft-delete marker
the other sys_ tables use: an installed-app registry has no "deleted by
accident, needs recovering" case, and a physical delete is what lets the
same code be installed again afterwards.

status is installing/installed/failed rather than a boolean, because an
install spanning several migration files is not atomic on MySQL - DDL
commits implicitly, so a run can stop in the middle. failed_version and
last_error are diagnostic snapshots for a person to read; nothing may
decide anything from them, and the field comments say so. Where to
resume is answered by sys_migration, which cannot drift from what was
actually applied.

sys_app_casbin_grant records which casbin_rule rows an install created,
keyed by casbin_rule's own natural key. That table is not extended
instead: gorm-adapter's SavePolicy truncates and reloads it from an
in-memory model, which would drop any column added here without a word.

Built against SQLite, MySQL 8.0 and PostgreSQL 15.
2026-09-08 19:26:40 +08:00
zhangwenjian ea348fa9d1 build📦: pin go-admin-core v2.8.0
v2.8.0 adds sdk/contract/app - an application's manifest, and the one
comparator for its version - which the installer in this batch is built
on. Nothing here uses it yet; this is the dependency arriving.

Checked that the release is consumable rather than only tagged: a
program built against the published module registers a manifest, reads
it back, and gets -1 from Compare("1.9.0", "1.10.0"), which is the
multi-digit case a string comparison would order backwards.

25 packages pass and checksilent is clean on the new version.
2026-09-08 18:35:13 +08:00
wenjianzhang 925c6772a6 Merge pull request #921 from go-admin-team/fix/920-schema-readiness
Fail readiness while the database is behind the migrations
2026-09-08 17:18:34 +08:00
wenjianzhang 0c60e44aee Merge pull request #922 from go-admin-team/fix/919-drop-index-postgres
Drop the index with SQL this dialect can parse
2026-09-08 17:18:08 +08:00
zhangwenjian a716086295 test: fail these tests when the database does not answer
Every Create and Raw dropped its error. Most of them would have failed
an assertion further down anyway, with a message describing the wrong
problem - but one of them would not.

The last count in the index test reads how many indexes survived and
expects zero. An unchecked query that fails leaves the variable at zero,
and zero is what success looks like: a test that cannot reach the
database reports that the indexes were dropped.

Demonstrated rather than assumed, by breaking that one query both ways:

    with the check:    FAIL  counting the indexes after: relation
                             "pg_indexes_nope" does not exist
    without it:        PASS

Raised by Copilot on #922, as a consistency point with the SQLite tests
in this package. It is that as well, but the reason it is worth doing is
the row above.
2026-09-08 17:04:31 +08:00
zhangwenjian f8a5066a40 test: run the migration against PostgreSQL in CI
The rest of this package's tests run on SQLite, where dropping an index
through the migrator works. That is why a migration which failed on
every PostgreSQL database it was pointed at had a green suite: the
defect cannot occur on the backend being tested.

A test alone would not have helped either - without a service it skips,
and a test that never runs is the same as no test. So the workflow gains
a postgres service and the DSN, and the helper refuses to skip when CI
is set: a workflow that drops the service or renames the variable fails
rather than going quietly green, which is the shape of the original
defect.

Counter-proved by putting Migrator().DropIndex back, which reproduces
the statement verbatim:

    DROP INDEX CURRENT_SCHEMA()."idx_sd_pg_user_deleted_at"
    ERROR: syntax error at or near "CURRENT_SCHEMA" (SQLSTATE 42601)

The conversion test also checks the timestamp survives as a marker,
since a conversion that dropped it would bring deleted rows back live
while still passing a column-type assertion.
2026-09-08 16:57:17 +08:00
zhangwenjian f978967ef1 fix🐛: drop the index with SQL this dialect can parse
The soft-delete conversion dropped the indexes on deleted_at through
gorm's Migrator().DropIndex. Its PostgreSQL driver resolves a schema for
the statement and falls back to an expression when it cannot:

    currentSchema, _ := m.CurrentSchema(stmt, stmt.Table)
    m.DB.Exec("DROP INDEX ?.?", currentSchema, clause.Column{Name: name})

DROP INDEX takes an identifier in that position, so what reached the
server was

    DROP INDEX CURRENT_SCHEMA()."idx_sys_api_deleted_at"
    ERROR: syntax error at or near "CURRENT_SCHEMA" (SQLSTATE 42601)

The schema is unresolvable for every call this migration makes, because
it passes a table name as a string rather than a model. So it failed on
every PostgreSQL database rather than intermittently, and stopped the
whole conversion at the first table.

What that looked like from outside is go-admin#919: an upgrade that
could not complete, and a login rejecting a correct password, because
deleted_at was still a timestamptz while the current query compares it
to 0. Neither symptom names a migration.

Written per dialect, for the same reason addBigIntColumn and
renameColumn already are. MySQL and SQL Server name the table and have
no IF EXISTS for it; PostgreSQL and SQLite name the index alone.

Verified by running the shipped migration against PostgreSQL 15 and
MySQL 8.0 in containers, and SQLite through this package's tests. The
SQL Server form is from its documentation and has not been run - there
is no SQL Server here to run it against, and saying so is better than
implying four dialects were checked.
2026-09-08 16:57:15 +08:00
zhangwenjian d6e2c02fda fix🐛: fail readiness while the database is behind the migrations
The process started, both probes passed, and the first sign that the
schema did not match was a login failing with a driver-level encoding
error - go-admin#919, where an operator upgraded the binary and
restarted the API without running migrate. Nothing between those two
events had an opinion about the schema.

Readiness is where this belongs. Liveness asks "restart me", and a
process whose database is on the wrong schema comes back to the same
schema. Readiness asks "send me requests", and the answer is no. A
rolling update then stalls at the deploy - new instances never become
ready, the old ones keep serving - rather than at somebody's login, and
running migrate clears it without a restart because the check is
evaluated per request.

Any tenant database being behind fails the check, not only the one being
served: migrations are applied to every database in one run, so one
behind means that run did not finish, and serving the rest would let a
half-applied deploy look like a partial success.

A missing sys_migration table is nothing applied rather than an error.
That is a first deploy, where every migration is pending and the
operator can act on being told so.

The one test that matters is the one that cannot be written normally.
The registry is filled by init() in packages cmd/api does not import, so
a test that imported them to look at it would pass whatever the real
binary links - and a binary that links none of them gives a check that
reports every database current, forever, with every other test here
still green. TestTheServingBinaryLinksTheMigrationRegistry asks the
build instead, with a negative control so that a query matching
everything fails rather than passes.

Closes #920.
2026-09-08 15:42:14 +08:00
zhangwenjian e98b65cf90 feat: let the host add a readiness check this package cannot make
Whether the schema matches what the binary expects is answered by the
migration registry, which lives under cmd/. common/ has never imported
cmd/, and starting with this would put the shared layer behind the
command layer for one check.

Register instead, from where both are already in scope. A duplicate name
panics rather than appending: two checks under one name make the failing
one impossible to identify from the response body, and registering the
same one twice is a wiring mistake better heard at start-up than never.

The registered check is run through the same guard as the built-in ones,
so one that panics fails its check rather than taking down the probe
that asked.
2026-09-08 15:42:14 +08:00
zhangwenjian bc5411c30c feat: report what the migration registry holds without a database
Status answers what is registered, what is applied, and what is applied
while nothing registers it - and needs a database to do it. A readiness
check needs only the first half, and it already holds the databases it
is asking about.

Without this it would have to call SetDb to reuse Status, writing this
package's shared state from a request path, for a question that does not
depend on any database at all.
2026-09-08 15:42:11 +08:00
wenjianzhang 27f23121f0 Merge pull request #918 from go-admin-team/fix/911-queue-shutdown
Drain the queue on the way out instead of abandoning it
2026-09-07 17:39:15 +08:00
zhangwenjian 047b23872c fix🐛: let a drain that finishes on the deadline count as finished
The wait was one select over done and ctx.Done(). Both can be ready when
it runs, select picks at random among ready cases, and so a queue that
drained in the same instant the budget expired was reported as an
overrun about half the times it landed there - often enough to be read
as noise, and pointing at the wrong thing when it was not. core's own
RunShutdown re-checks for this reason; this did not.

The tie-break is now a function taking channels rather than a queue,
which is what lets a test hand it a closed done and an expired ctx
together. That state is the whole of the bug and cannot be arrived at
reliably from the outside; over 1000 iterations the single-select
version fails, and the second look does not.

The test for giving up on the deadline read the call counter straight
after shutdownQueue returned, while Shutdown runs on a goroutine nobody
joins. It passed because the goroutine is scheduled promptly, not
because anything ordered the two. The fake now signals that Shutdown has
been entered and the test waits for it.

Both raised by Copilot on #918.

The first attempt at the tie-break test was wrong and is not what
landed: it asserted that an immediately-returning Shutdown always counts
as drained under an already-expired context, which is not true and
should not be - if the goroutine has not run, nothing has drained. That
test failed, correctly. What is being claimed is narrower: when both are
ready, done wins.
2026-09-07 17:21:03 +08:00
zhangwenjian 84bd87dcc9 fix🐛: drain the queue on the way out instead of abandoning it
Nothing stopped the queue when the process exited. core v2.7.0 made the
drain work - Memory.Shutdown closes the queue and waits for every
consumer to finish what it holds, and the legacy adapter cancels its
context and closes the underlying queue - but no call site ever reached
it. The only Shutdown() in this repository applies to the previous
adapter during a reload, so the installed one was simply left. The login
log, the operation log and the API sync all publish through it, so a
rolling restart dropped whatever had not been consumed, on the path
where the process exits 0 and reports "Server exiting".

Setup now registers a BeforeExit callback that shuts down the adapter
this package installed.

Three things it has to get right, each with a test.

It reads `installed` when it runs, not when it registers. A reload
replaces the adapter, and the one from start-up is a queue nobody has
published to since.

It never goes through sdk.Runtime.GetQueueAdapter. That accessor never
returns nil - with no queue section configured it wraps the runtime's
own fallback - so it would look like it worked while closing a queue
this package neither built nor started. That is the same trap setupQueue
already had to drop an `if q != nil` for.

It registers once. Setup is re-run on every configuration change, and a
callback per reload would leave the shutdown phase holding a row of
identical entries, each eligible to be named as the one that overran the
budget.

That last one needed a seam. shutdownQueue takes the adapter on its
first run, so the second and third callbacks find nothing and return -
three registrations produce exactly the same observable result as one,
and a test going through the effect passes either way. It did: the
counter-proof for "register on every reload" came back green until the
registration was counted at the seam instead.

The wait is bounded here rather than left to the phase. Shutdown takes
no context, so a consumer that never finishes would hold the process
until SIGKILL; the callback gives up and says what is being lost, which
the phase's generic overrun message cannot.

Ordering falls out of the phase rather than being arranged: callbacks
run in reverse registration order, this one registers during setup and
the job scheduler's registers on AfterListen, so the schedulers stop
before the queue drains. Verified against core v2.7.0 rather than read
off the source.

Closes #911.
2026-09-07 17:10:13 +08:00
wenjianzhang 0e7a13aeba Merge pull request #917 from go-admin-team/fix/914-gen-write-guard
Register the generator's writing endpoints only in a development mode
2026-09-07 15:32:13 +08:00
zhangwenjian 85d50da494 fix🐛: tell the start-up warning's reader to restart, not only to reconfigure
The warning said to set application.mode and stopped there. Following
that on a running process does not close anything: buildRouter has one
call site, in run(), and route registration is on no phase and no reload
callback, so a configuration reload moves the mode and leaves the routes
exactly where they were.

The reader is then worse off than before they acted. The mode now says
prod, GenWriteRoutesEnabled agrees, and the endpoints are still served -
so the one thing they could check to confirm the fix reports success
while the exposure is untouched, until something restarts the process.

A test pins the gap rather than the prose: build under dev, move the
mode to prod, and the routes are still in the engine. It fails if
registration ever becomes dynamic, which is the change that would make
the new sentence wrong.

That test degrades differently from the others - making it fail means
rewriting registration, not weakening it - so what was checked instead
is that it cannot go vacuous. Both of its premises are guarded: with the
gate always refusing it reports building under dev without the writing
routes, and with the gate always allowing it reports the predicate still
allowing prod. Neither failure can be mistaken for the assertion passing.

Raised by Copilot on #917.
2026-09-07 15:20:36 +08:00
zhangwenjian 1d9def4314 test: restore the mode before the route helper returns
registeredRoutes set config.ApplicationConfig.Mode and gave it back with
t.Cleanup, which runs at the end of the test rather than at the end of
the helper. Everything the caller did after the call therefore ran under
the mode the helper had been asked about, not one the caller chose.

Nothing was wrong yet: the one caller that reads the mode afterwards
sets it itself, and the two cleanups happen to unwind in an order that
leaves the right value. Both of those are accidents, and neither is
visible at the call site.

A defer inside the helper makes the borrowing end where it starts. The
doc comment said the mode was put back before returning while the code
did not, so that is now true rather than aspirational.

The counter-proof is the reason this has a test of its own: with
t.Cleanup back in place TestRegisteredRoutesRestoresTheModeBeforeReturning
fails and nothing else does, which is what a leak this quiet looks like
when something is actually watching for it.

Raised by Copilot on #917.
2026-09-07 15:14:39 +08:00
zhangwenjian ed9bbd01e2 feat: warn at start-up when the generator can write to this host
The gate in the previous commit is decided by application.mode, and the
shipped configuration says dev. So the deployment most likely to be
serving the writing endpoints is the one that changed nothing, and that
is also the one least likely to go looking for them. A gate whose
default is open needs to say so.

Nothing is said in demo mode. The routes are registered there, but
DemoEvn refuses all three by name, so a warning would describe an
exposure that is not present.

The decision is split from the logging so it can be tested. Three
counter-proofs: warning in demo as well fails mode=demo; a warning that
never fires fails mode=dev, which is what shows the line can be reached
at all; and one that always fires fails every mode but dev.
2026-09-07 15:07:07 +08:00
zhangwenjian 523d6a3649 fix🐛: register the generator's writing endpoints only in a development mode
Three of the code generator's endpoints do not read. /gen/toproject
writes seven Go and Vue source files onto the host, one of them under
the path gen.frontpath names; /gen/apitofile writes a migration;
/gen/todb inserts menus and APIs. All three are GET, and all three are
listed in CasbinExclude - which AuthCheckRole skips - so Enforce never
runs for them. Any account that can log in could call them, on every
deployment.

They are now registered only where application.mode is dev or demo. dev
is the shipped default and is where the generator is meant to be used.
demo keeps them because demo mode already has a better answer than a
404: DemoEvn refuses these three by name and explains itself, which is
what the demo is for. test and prod get nothing, and so does a process
whose mode was never set.

This does not make the endpoints safe where they exist; it stops them
existing where nobody should be calling them. A host left on the shipped
dev is still open, which is why the next commit says so at start-up.

CasbinExclude is left alone on purpose. Taking the three off that list
would make them require a permission no existing deployment has granted,
so every non-admin user would start getting 403 from a tool that worked
yesterday. That is a migration, not a guard, and it belongs with a
release that can carry one.

Four counter-proofs, each red on the test that names the behaviour and
green everywhere else: a gate that always allows fails test/prod/unset
only; a gate that always refuses fails dev/demo and takes
TestEveryRouteDemoModeRefusesStillExists with it; moving a read-only
route inside the gate fails the reading test; and spelling the condition
at the registration site instead of calling the predicate fails the
agreement test, which is what keeps that test from being a tautology.
2026-09-07 15:05:24 +08:00
zhangwenjian 6326962862 style🎨: gofmt gen_router.go
Two pre-existing deviations: a space before the comma in
sysNoCheckRoleRouter's parameter list, and no newline at end of file.
Separated from the change that follows so its diff is only the change.
2026-09-07 15:02:33 +08:00
wenjianzhang cd7c8375c0 Merge pull request #916 from go-admin-team/docs/replicas-constraint
Name the job scheduler as the other reason for one replica, and stop the manifests redeploying the demo
2026-09-07 14:52:53 +08:00
zhangwenjian 63bcc912ef ci👷: stop the k8s manifests from redeploying the demo
The comment at the top of this workflow says documentation-only changes
skip it, because a push to master pushes an image, runs the migrations
and restarts the demo container. The ignore list did not cover
scripts/k8s, so editing a manifest that the deploy never reads bought
the site an outage.

The pattern is scripts/k8s/** rather than scripts/** because
scripts/Dockerfile is a build input - go.yml builds the release image
from it on a tag.

This workflow file stays outside the list on purpose. paths-ignore skips
only when every changed path matches, so a change that edits the deploy
still runs it, which is the point.
2026-09-07 14:14:39 +08:00
zhangwenjian adcdd2edcd docs📝: name the job scheduler as the other reason for one replica
The comment on replicas gave one obstacle to raising it, the shared log
volume, which reads as the only one. Someone who moves the log path off
that volume would conclude the way is clear.

The scheduler in app/jobs is the second, and it is the one that does not
announce itself. Its handle on a job lives in sys_job.entry_id, one
column shared by every process, and startup zeroes the whole column
before writing its own ids. A second pod therefore erases the first
pod's, and both pods run the full enabled list. Stopping a job from the
UI then removes an entry from whichever process is asked, by an id that
belongs to another one, and answers 200.

See #915.
2026-09-07 13:32:43 +08:00
wenjianzhang 29406f839e Merge pull request #913 from go-admin-team/fix/demo-mode-guard
fix🐛: demo 模式放行了注册成 GET 的写接口
2026-09-07 08:11:14 +08:00
zhangwenjian a43133ab7b fix🐛: stop demo mode serving the writes that are registered as GET
DemoEvn decided by HTTP method: GET and OPTIONS through, everything else
refused. Three of the code generator's routes are registered as GET and write
anyway - two emit Go source files onto the server's filesystem, and the third
inserts menus, APIs and casbin rules into the database. They sit in a group
whose own name says it does no role check, and a demo deployment lets anybody
log in. So on the demo host any visitor could write to the machine and to the
database, and the one that writes menus had in fact been used: three generated
SysCasbinRule entries is how this was noticed.

The guard now also looks at the matched route. The method cannot answer the
question - whether a request changes anything is not something the verb reports
truthfully here - so the three are named, as gin route patterns, which is what
Context.FullPath returns and how CasbinExclude already spells them.

Changing them to POST would be the better shape and is not this change. sys_api
records an endpoint by method and path and the casbin policy follows it, so
flipping the verb needs a migration and a policy resync; until both land, every
existing deployment would start answering 403 to a role that could use the
generator the day before.

The read-only half stays reachable: preview, the table tree, and the two
database listings. A demo host that cannot demonstrate the generator is as
broken as one that lets visitors write to it - refusing too much is the same
defect facing the other way, and there is a test for that direction too.

Half of the general hole is closed and the other half is written down. The
closed half is a test beside the route registrations: it builds the generator's
routes, enumerates them, and fails if any entry in the guard has stopped being
a real route, so renaming one turns the list red instead of quietly making it
match nothing. It lives there because common/ may not import app/ - which is
also why the guard cannot check its own list from where it is. The open half is
that no static check can tell a handler that writes from one that reads, so the
next GET that writes has to be added by hand. The comment says that rather than
leaving the impression the class is covered.

application.demomsg was configuration nothing read. The message was hard-coded
in the middleware, and the demo host's configured string happened to be
identical, so the setting looked like it worked and never had. It is read now,
with the old string kept verbatim as the fallback, so a deployment that never
set it is answered exactly as before.

This covers demo mode only. On a deployment that is not a demo those three
routes remain in CasbinExclude and stay reachable by any authenticated user
whatever their role; that is a separate decision and is not touched here.
2026-09-07 08:03:40 +08:00
wenjianzhang 7002cd4065 Merge pull request #912 from go-admin-team/feat/007-drain-window
feat: 关闭时先排空再停止接收——让 /ready 的 503 真的能被采到
2026-09-07 08:01:59 +08:00
zhangwenjian 8faa8d2aed feat: check the shutdown budget against every stop deadline
The budget is one number in config/settings.yml. The deadlines that have to
cover it are in four other files, none of which anybody edits while thinking
about shutdown - so raising the budget passes every test, deploys, and has the
cleanup callbacks killed on the next release.

Two checks share one arithmetic and one five-second margin.

shutdown-budget-overruns-grace compares preStop + drain + server + cleanup
against terminationGracePeriodSeconds in the shipped manifest. Those two files
are not merely adjacent examples: scripts/k8s/prerun.sh builds the
settings-admin ConfigMap out of config/settings.yml and the Deployment mounts
it, so the manifest deploys that file.

docker-stop-cuts-shutdown-short covers the three ways this container is
stopped: `docker stop` in the release workflow, the same in the Makefile, and
stop_grace_period on a compose service that runs this repository's own image. A
service running a database is not this process and is left alone. The duration
is parsed rather than scanned for digits - compose accepts 1m30s, and reading
the first number out of it would call ninety seconds one.

All three spellings of the deadline are read: --timeout, the deprecated --time,
and the short -t. A deadline the check cannot read is reported as no deadline at
all, so recognising only one of them would call a correct command broken and
send whoever fixed it towards the spelling docker is retiring. The message
quotes the flag back in the spelling it was written in, for the same reason:
suggesting a flag the line does not use is how a tool teaches people to
disbelieve it.

What neither covers is `docker rm -f`, which has no deadline to compare
against: it is SIGKILL by definition. That gap is deliberate, and it is why the
previous commit changed the one place that used it on a container that might
still be running.

Both report at two levels. A budget that already overruns is an ERROR; one that
fits with nothing to spare is a WARN, because it works today and failing the
build on a working configuration is how a project teaches people to ignore its
warnings. The two are exclusive: an overrun satisfies the headroom condition as
well, and an ERROR that always drags a duplicate WARN behind it teaches the same
lesson.

preStop is in the sum although the shipped manifest has no hook. That is the
point - a hook added later is spent before the process is told anything, and a
self-check that could not see it would understate the real budget by however
long somebody set it to, which is worse than not checking. A hook whose duration
cannot be read is reported rather than counted as zero.

The fallbacks for fields the settings file leaves out are read from the
constants in the scanned tree, not copied here; if they are renamed the run
stops instead of going quiet with the wrong numbers.

The wording differs by audience on purpose. At run time this is somebody else's
deployment under constraints the process cannot see, so the log states a
minimum. These checks read files this repository owns, where there is standing
to ask for headroom, so they name a target.

The table in AGENTS.md is relisted while it is being touched: the two new
checks, plus datascope-route-unguarded, which has been missing since it was
added. The hard-coded count is gone - it said seven and there were ten, which is
what a written-down count does. AGENTS.md and docs/contract.md both sent readers
to `go run ./tools/checksilent -h` for the list of checks; that prints
command-line flags and has never printed a check, so both now point at
runChecks.

The yaml parser moves from an indirect requirement to a direct one - it was
already in the module graph - and tidy drops four go.sum lines left over from
two older releases of core.
2026-09-06 22:07:08 +08:00
zhangwenjian 705427178d fix🐛: give every stop path enough time for the shutdown budget
Stopping this process takes drain + server + cleanup seconds: eight out of the
box, and more for anyone who configures a drain window. Three places decide
whether it gets that long, and none of them was written with it in mind.

The release workflow stopped the previous container with the default deadline,
which docker sets at ten seconds. The compose file - which the Makefile calls
the first way to run this - set no stop_grace_period, so it took the same ten.
Under either, a drain window over two seconds would have been cut off by
SIGKILL part-way through the cleanup callbacks: this project's own deployments
could not have run the capability it ships.

The third was worse. `make run` removed the previous container with `docker rm
-f`, and the force flag kills a running container outright - "uses SIGKILL", in
docker's own words - with no grace at all. Restarting locally cut every
shutdown short, so the drain window would never once have been reached on a
developer's machine. It now stops with a deadline and then removes, which
leaves what gets removed unchanged: on a container that has already stopped,
stop is a no-op.

So: --timeout 30 in the workflow, stop_grace_period: 30s on the compose
service, and stop --timeout 30 before the removal in the Makefile. --timeout
rather than --time, which docker still honours but has deprecated - it prints a
warning on every use, and a deploy log that always carries a warning is one
nobody reads.

The three remaining `docker rm -f` calls in the workflow are left alone. Two
remove containers that have already been stopped and one is the rollback path,
and nothing static can tell those apart from a container that is still running -
which is also why the check added next does not look at `rm -f` at all: a forced
removal has no deadline to compare against. What keeps that path honest is the
line above it, not a check.

Thirty will drift the first time somebody raises a budget. The next commit is
what notices, which is also why these comments name a check that does not exist
yet.
2026-09-06 22:06:35 +08:00
zhangwenjian 8f10d202e6 feat: give the shipped manifest probes and a stop grace period
The manifest in this repository had no probes at all. A pod was sent traffic as
soon as its container was running, whether or not the database it needs was
reachable, and it was stopped with whatever grace period Kubernetes defaults to
rather than one chosen against what this process actually spends shutting down.

It now mounts both probes, at the endpoint that answers each question:
readiness at /ready, which fails while a dependency is unreachable, and
liveness at /health, which is a bare 200 because restarting a process whose
database is down turns one outage into a crash loop. Both skip the rate
limiter, which is why that had to land first.

timeoutSeconds is 3, not the default 1. The handler allows its checks two
seconds, so at the default a database answering in 1.2s would be recorded as a
failed check while the handler was returning 200 - the probe would be failing on
the orchestrator's stopwatch, not on its own. The comment beside that constant
said the constraint was the polling period; the constraint is the per-check
timeout, and it is now written down correctly.

terminationGracePeriodSeconds is 30, against a shipped budget of 0 + 5 + 3.
Raising drain means raising this too, in the same commit; the check that
notices when somebody does not arrives two commits from here.

replicas stays at 1, and the comment says why that makes the drain window worth
nothing: there is nowhere to send the traffic this pod stops taking. Raising it
needs one more change than the number - the volume is shared by every replica
and the log path lives on it, so a second pod would append to the same rotating
file. The reason not to raise it is not the one the review assumed: the claim
was that the PVC is ReadWriteOnce, and it is not, it is ReadWriteMany on nfs-csi.

There is no preStop hook. How long one should sleep depends on how fast the
thing in front removes this instance, which the repository cannot know, and a
manifest carrying both a preStop sleep and a drain window is the double-counting
trap - the budget would be spent twice and the start-up line would report half
of it.
2026-09-06 21:49:17 +08:00
zhangwenjian 5648bd1dcf feat: state the shutdown budget at start-up
The three budgets are spent one after the other, so what has to fit inside the
orchestrator's grace period is their sum - and nothing said what that sum was.
Working it out meant reading a configuration file, remembering which fields
were absent, and knowing what each one falls back to.

Start-up now prints it: the three values and the total, taken from the resolved
budget rather than from the file. A field left out still costs its default, so
adding up what was written down understates the total by exactly the fields
nobody wrote - which is the arithmetic somebody doing this by hand gets wrong.

Whether the total fits is a separate question, and the framework cannot answer
it alone: `docker stop` allows ten seconds and Kubernetes thirty, three times
apart. A fixed threshold would have warned about the manifest this repository
is about to ship. So extend.shutdown.grace is optional, nothing reads it during
a shutdown, and when it is absent the line says so and prints both reference
values instead of judging.

When it is set and the budget does not fit, the warning names the shortfall:
how many more seconds are needed. A minimum, not a target - this is somebody
else's deployment under constraints this process cannot see, and asking them to
leave headroom on top is not this line's business. Equal does not fit either;
the grace period is when SIGKILL is sent, so a budget that ends exactly then
leaves the last callback no time to return.
2026-09-06 21:49:17 +08:00
zhangwenjian a442eadb96 feat: keep serving for a configurable window before the listener closes
/ready has failed from the moment shutdown begins since the readiness probe was
added, and the order it does that in is right: reversed, the state would be
reported after the connections were already cut. But order alone does not
produce a window. Nothing waited between the flip and Shutdown, so the two were
microseconds apart, and a poller on a multi-second interval never saw the 503 -
it saw a refused connection, which is the thing the probe was supposed to
avoid. Polling a container through a SIGTERM on the demo host recorded exactly
that: 200, then connection refused, and no 503 in between.

extend.shutdown.drain is that wait. The process keeps serving normally for it -
answering requests, not refusing them, because refusing them would move the
outage earlier rather than avoid it - and only then closes the listener.

It is zero by default, so nothing changes for a deployment that does not ask
for it. That is not timidity: the budgets are spent one after another, and a
non-zero default would push every existing shutdown closer to the orchestrator's
grace period, where being cut off part-way through the cleanup callbacks is
worse than never draining at all.

Keep-alive is switched off with the flip. The server keeps connections alive
until Shutdown sets shuttingDown() itself, so without this the pooled
connections a balancer holds would sit untouched for the whole window and be
cut at the end of it anyway - the cost of the window without its benefit. This
is the switch Shutdown flips, moved earlier by the window's length.

The signal disposition is restored after the window rather than on the first
signal. Before there was a window, the interval where a second signal killed
the process outright was only reachable while a cleanup callback hung; putting
a multi-second wait inside it would have made every ordinary shutdown
interruptible for the length of the drain. A second signal during the window is
taken by the channel and ends the window early instead - somebody sending
another kill wants this over with sooner - and the escape hatch comes back the
moment the window does.

What the window is worth depends on who removes this instance. A balancer that
polls /ready acts on the 503 and needs the window to cover its check interval
times its failure threshold; a Kubernetes Service withdraws the endpoint when
the Pod is deleted, concurrently with SIGTERM and regardless of what the probe
returns, and there the window covers the delay in that removal reaching every
node. The three comments that used to say a balancer "has a chance to" take the
instance out said it without either qualification, which is how a claim comes to
be repeated after a live test has refuted it.

The subprocess test polls the real probes on a connection it opens after the
signal - a reused one can be served after the listener is closed, which would
let this pass against a shutdown that had already broken it - and asserts on the
draining answer in the body, not on the status code. With no database the status
is 503 from start-up, so a status-code assertion would hold even with
BeginDraining deleted. Two window lengths, because one proves only that
something takes that long.
2026-09-06 21:49:17 +08:00
zhangwenjian f3b67e9abc fix🐛: keep the rate limiter away from the health probes
The limiter is installed on the engine and the probes are routes like any
other, so above the threshold they are answered with 429 too. Point a liveness
probe at one and the failure mode writes itself: traffic crosses the threshold,
the probe collects three 429s, the kubelet restarts the container, the capacity
that was already short gets shorter, and the instances that are left are pushed
further past the threshold. The limiter working exactly as designed is what
kills the pod.

It is the argument common/health already makes about restarting a process whose
database is unreachable, applied to load: turning one outage into a crash loop
is not an improvement on the outage.

Nothing points a liveness probe at these routes yet. The manifest that will is
two commits away, and this has to land first, because that manifest without
this change would be actively harmful.

The exemption wraps the middleware rather than teaching the limiter about these
paths. common/ may not import app/ - the contract check enforces it - so the
limiter cannot name routes that are registered over there. Wrapping it in the
command package, which imports both, is what keeps the boundary.

Naming those routes needs them exported, so the group prefix and the two paths
become constants and the router function becomes RegisterMonitorRouter. That
also gives a test something real to mount: a probe asserted against a
re-implementation of itself is a test of the copy.

The check that the middleware never runs is separate from the check that the
answer is not 429, because a probe can produce a 429 on its own. What has to be
true is that the request never reached the limiter.
2026-09-06 21:49:16 +08:00
zhangwenjian 4e51f56623 feat: make the shutdown budgets configurable
How long a shutdown may spend waiting for in-flight requests, and how long the
cleanup callbacks get after that, were compile-time constants. The two together
have to fit inside whatever grace period the orchestrator allows before it
sends SIGKILL, and that number is not the same everywhere - `docker stop`
allows ten seconds, Kubernetes thirty by default - so the one deployment shape
these constants suited was the one they were written for.

They now come from extend.shutdown, beside rateLimit. Not from application:
that section is a fixed struct in core, and the decoder discards keys it has no
field for without an error, so a budget written there would be accepted and
never applied. That is the failure this whole change is about, and putting the
configuration where it cannot be read would have reproduced it.

Both fields are pointers, following RateLimit.InboundQPS: nil means "not
configured" and takes the default, and a number that was written down is spent
literally, zero included. Without that separation `server: 0` - do not wait for
in-flight requests at all, which is a reasonable thing to ask when the grace
period is very short - could not be expressed, and the section would need a
paragraph explaining which zeros mean what.

A negative is refused rather than clamped. Correcting a value quietly is the
same failure in a different costume, and Budget returns the error instead of
ending the process so that the rule can be tested without a subprocess.

The defaults live in config as seconds and in cmd/api as durations, both from
the same constants, and a test asserts the two agree - a deployment that
configures nothing is entitled to one answer about what it spends, not two.

The last test loads the two settings files this repository ships through the
real loader and asserts the section arrives with the documented values. Nothing
weaker can tell "the key is read" from "the key is discarded": the struct
compiles either way.
2026-09-06 21:49:16 +08:00
zhangwenjian 7e4e17bbcf test: run the real shutdown sequence in the signal tests
The child process built a server, restored its own signal disposition and
called shutdownServer and runShutdownHooks itself, in an order it chose. It
never called anything run() calls. So the assertions were about a copy of the
sequence: move a step in the real one, or drop it, and every test here stays
green. The acceptance criteria these back are worth exactly as much as that.

The child now calls gracefulShutdown and asserts on what comes out of it. The
budget it spends is defaultBudget with one field shortened where a test needs a
deterministic timeout, which is also how the two waits stop being wired by
hand.

The stuck-shutdown case changes shape as a result. It used to sleep inside the
child, between the steps it had copied; there is no "between" to sleep in any
more, so it registers a BeforeExit callback that never returns and gives it a
budget long enough to hang on. That is where a shutdown actually hangs, and it
now runs through the same function - which means this test also pins where the
signal disposition is restored, rather than just asserting that the child dies.

It signals repeatedly rather than once. The marker is printed immediately
before gracefulShutdown is entered, so a single signal sent on seeing it can
still arrive before the disposition is restored, land in the buffered channel
and be dropped. Which signal does the killing is not the assertion; that one of
them can is.
2026-09-06 21:49:16 +08:00
zhangwenjian 799e892a68 refactor♻️: run the shutdown sequence from one function
The steps between the stop signal and the last log line were written inline in
run(), which left nothing for a test to call. The signal tests reproduce that
sequence instead: they build their own server, restore their own disposition,
and call shutdownServer and runShutdownHooks in an order of their own. So they
assert against a copy - reorder the real sequence, or drop a step from it, and
they stay green.

The sequence now lives in gracefulShutdown, and the waits it spends are a
budget rather than two constants read at the point of use. Nothing changes
about what happens or in what order: the same disposition is restored first,
the same two announcements are made, the same waits are spent, and run() logs
the same two errors with the same messages.

Returning those errors instead of logging them inside is what lets a caller
other than run() react to them. That matters for the next commit, where the
tests stop reproducing this sequence and start running it.
2026-09-06 21:49:16 +08:00
wenjianzhang 0e2adb3165 Merge pull request #909 from go-admin-team/fix/queue-swap-order
fix🐛: 热重载期间生产者被指向已关闭的队列
2026-09-06 21:23:01 +08:00
wenjianzhang 249e044ded Merge pull request #910 from go-admin-team/docs/readiness-claim
docs📝: 摘除实例的不是 readiness——订正 #908 的三处注释
2026-09-06 20:32:51 +08:00
zhangwenjian d6309c75be docs📝: state what the draining answer is worth
Three comments said readiness failing before the server stops accepting gives
a load balancer the chance to withdraw the instance before its connections are
cut. Nothing between the two lines makes that possible: BeginDraining is
immediately followed by the shutdown, and a poller on a multi-second interval
never observes the flip.

On Kubernetes the endpoint is withdrawn when the Pod receives a
deletionTimestamp, concurrently with SIGTERM and independent of what the probe
returns, so the probe result is not the mechanism there either.

The order itself stands - reporting the state after the connections are cut is
worse - so the comments now say the order is necessary and not sufficient, and
that a window needs a configured delay that does not exist yet.
2026-09-06 19:04:42 +08:00
zhangwenjian 211ae85a4e test: fail on an Append error this test does not model
Only ErrQueueClosed was examined and every other error was discarded, so a
run where nothing reached a queue at all would leave the refusal count at
zero and the test green. The first unexpected error is now kept and fails
the test.

The pool is sized so a full queue cannot be one of them: it returns an error
of its own and is expected while the consumer is held, which would otherwise
make the new check fire on the normal path.
2026-09-06 19:00:30 +08:00
zhangwenjian 3c3d94ca76 test: wait for the sample rather than for the clock
Two fixed sleeps decided when this test looked: one to let the consumer pick
a message up, one to let publishes accumulate inside the reload. Both are
guesses about how fast the runner is. The consumer now signals on its first
delivery, and the measurement waits until enough publishes have landed.

The "nothing was published" guard goes with them. It was the weaker form of
the same check, and it ran after the fact instead of holding the window open
until there was something to measure.
2026-09-06 19:00:25 +08:00
zhangwenjian 6138d2d74c test: assert the bound installing first actually gives
The assertion demanded zero refusals during a reload, and this ordering
cannot deliver that. GetQueuePrefix returns a wrapper that captured the
adapter, so a producer that fetched before the swap and appends after
Shutdown has begun is still holding the old queue. That window is one call
wide; closing it means resolving the adapter inside Append, which is core's
to change.

What the ordering removes is the sustained window - every producer that
fetches during the wait. Measured on a race-enabled run: 174 of 174 publishes
refused with the old order, 1 of 174 with the new one. The old assertion
therefore failed about half the time on a change that works.
2026-09-06 19:00:20 +08:00
zhangwenjian d5de79f75b test: join the producer before the test returns
The publishing goroutine was told to stop and never waited for. t.Cleanup
restores sdk.Runtime while a producer that has not yet noticed the stop is
still reading it, which -race reports as a write and a read on the same
package variable. Signalling is not joining.
2026-09-06 19:00:10 +08:00
zhangwenjian 46e793972c fix🐛: install the new queue before shutting the old one down
Shutdown now waits for its consumers to deliver what the queue still holds, and
setupQueue called it first. For that whole wait sdk.Runtime still pointed at the
adapter that had stopped accepting, so every Append landing in the window came
back ErrQueueClosed - and both call sites in common/middleware log that at error
level while the row never reaches the database.

Measured with a held consumer: 177 of 178 publishes during one reload.

Installing first leaves no window. A producer fetches the adapter per call and
gets either the new queue or the old one, and both accept; the old one still
drains, because Shutdown is what waits for that.

The difference only exists during the wait - after Setup returns the two orders
look identical, which is why the test holds a consumer and publishes throughout
the reload rather than checking the state afterwards.

The counter-proof compiles and reports the 177.
2026-09-06 17:37:01 +08:00
zhangwenjian 46f4092b43 build🔧: require go-admin-core v2.7.0
It carries the queue shutdown fixes: a closed queue now delivers what it already
accepted and stops the goroutines consuming it. Both matter here, because this
host rebuilds its queue adapter on every configuration reload and shuts one down
on the way out.

Nothing in this commit uses the new behaviour. The one place that has to change
because of it follows.
2026-09-06 17:37:01 +08:00
wenjianzhang 196195357b Merge pull request #908 from go-admin-team/feat/readiness-probe
feat: 新增 /ready,并让它在关闭一开始就失败
2026-09-06 10:07:45 +08:00
zhangwenjian c579c5f84c fix🐛: give every cache probe its own key
One fixed key meant two probes overlapping - two /ready requests, or two
instances against the one redis, which is the normal deployment - overwrote each
other's value between the write and the read, and each concluded the cache was
broken. A readiness probe that reports false negatives under load pulls healthy
instances out of the pool, which is worse than not probing.

The key now carries eight random bytes. The written value is still read back,
because that is what tells a healthy cache apart from one that answers "miss"
for everything, and the key is deleted afterwards on a best-effort basis - its
error is dropped deliberately, since the verdict is already decided and a cache
that cannot delete what it just wrote is not a reason to refuse traffic.

The first version of the concurrency test had no teeth: the probes are short
enough that the scheduler ran them one after another, so the fixed-key
counter-proof passed three times out of three. The fake cache now holds every
writer until all of them have written, which makes the interleaving the test is
about actually happen. With one key that is a deterministic 15 failures out of
16 - only the last writer's value survives - and with a key per probe, none.
2026-09-06 09:55:12 +08:00
zhangwenjian 241c27358b feat: answer readiness separately from liveness, and fail it while draining
/health returned 200 without asking anything. Whatever it was meant to say, an
orchestrator reading it learned only that a process was accepting connections.

The two questions are not the same one, and the answers differ:

  - /health stays a bare 200. It answers "should I restart you", and a process
    whose database is unreachable does not want restarting - that turns one
    outage into a crash loop and discards the connection pool, the cache and
    every request in flight on the way.
  - /ready is new. It answers "should I send you requests", fails while a
    dependency is unreachable, and fails from the moment shutdown begins.

That last part is what the life-cycle phases bought. BeginDraining sits next to
BeginShutdown, before the server stops accepting, so a load balancer is told to
stop sending while this instance can still finish what it holds. Reversed - and
that is where it was - the connections are cut first and the probe reports it
afterwards.

The queue is deliberately not checked. Nothing on AdapterQueue answers "are you
reachable" without publishing something, the memory backend cannot fail, and a
queue that is down degrades logging rather than stopping requests: a reason to
alert, not a reason to leave the pool.

The cache probe writes and reads back rather than only reading. A cache that
answers "miss" for every key - a client pointed at the wrong server - is
indistinguishable from a healthy one on a read alone.

Every check runs behind a recover, and that is not defensive habit. The test for
"nothing configured" found the reason: GetCacheAdapter builds a wrapper around
whatever is configured and returns it even when nothing is, so the value is not
nil, the cache inside it is, and Set dereferences it. A nil check cannot see
that, and GetQueueAdapter behaves the same way. Whatever the cause, a probe is
the last thing that should be able to take the process down - the caller is
asking whether this instance is well, and killing it is the wrong reply.

The counter-proof compiles and fails: without the recover, the unconfigured case
panics rather than reporting two failed checks.
2026-09-06 09:47:55 +08:00
wenjianzhang aa3c9866cb Merge pull request #907 from go-admin-team/test/892-reload-generation
test: 补上 #892 修复中没有断言的那一半
2026-09-06 09:47:07 +08:00
zhangwenjian 2ac01ea584 test: restore what Setup writes outside this test
Setup installs the cache and queue adapters on sdk.Runtime, which is a
package-level singleton, and records what it installed in this package's own
variables. The test left all of it behind, so what a later test in this binary
saw depended on whether this one had run - the leak cmd/api's freshRuntime and
common/middleware's copy of it exist to prevent.

sdk.Runtime is swapped for a fresh one and everything is put back in Cleanup.
2026-09-05 23:18:01 +08:00
zhangwenjian 7f9cc1e435 test: a configuration reload installs a new queue for the consumers to notice
Issue #892 is that a reload replaces the queue adapter and the consumers
registered against the previous one are left attached to a queue nobody
publishes to any more.

The fix has two halves and only one of them was covered. attachQueueConsumers
gives a new queue its own consumers and the same queue none, which cmd/api
tests against a queue it controls by passing generation numbers in by hand. What
nothing asserted is that a reload actually produces a new generation for it to
notice - the half that lives in this package.

Setup is what config re-runs on every change, so calling it twice is what a
reload does here. The generation goes 0, 1, 2.

The counter-proof compiles and fails: dropping the increment in setupQueue
reports that the first Setup installed no queue at all, because a generation
that never moves is indistinguishable from never having been set.

This closes the loop rather than adding coverage for its own sake: with both
halves asserted, the claim that #892 is fixed rests on tests instead of on
reading the two functions and believing they meet.
2026-09-05 23:01:27 +08:00
wenjianzhang 4fb0529d2d Merge pull request #906 from go-admin-team/feat/005-host-wiring
feat: 生命周期阶段接线,并修掉队列注册顺序与从未停止的 cron
2026-09-05 22:49:50 +08:00
zhangwenjian 8ee4141af6 fix🐛: check the certificate before announcing that the port is reachable
AfterListen promises a hook that the port answers. The bind was moved onto the
caller's goroutine to keep that promise, but with ssl enabled there was a second
way to fail after the announcement: ServeTLS reads the certificate files itself,
on the serving goroutine, so a bad path or an unreadable key surfaced once the
hooks had already run.

tls.LoadX509KeyPair now runs before anything is announced, and its error is
returned from startServing the way a failed bind is. ServeTLS still does the
real work - handing it a tls.Listener built here instead would take over the
HTTP/2 negotiation it sets up, and quietly drop h2 for every TLS deployment. The
cost is one extra read of the certificate at startup.

The fatal in the serving goroutine said "listen:". Neither the bind nor the
certificate reaches it any more, so it says "serve:".

The test covers the certificate path alongside the bind: neither may announce
the phase, and neither may seal it.

The counter-proof is not clean, and saying so is the point. Removing the check
does turn the run red, but through log.Fatal killing the process from the
serving goroutine - "fatal serve: open no-such.pem: no such file or directory" -
rather than through the assertion. That still demonstrates the defect, because
the process could only get there after startServing had returned successfully
and the phase had been announced; it cannot be observed from inside the test,
because the fatal races the assertion that would report it.

Also: the redis-backed queue tests now fail instead of skipping when CI is set
and GO_ADMIN_TEST_REDIS_ADDR is not. A workflow that renamed the variable or
dropped the service would otherwise stay green while those two tests quietly did
nothing - the same shape as the defect they exist to cover. Locally, with no CI
in the environment, they still skip.
2026-09-05 22:41:53 +08:00
zhangwenjian 36f2549172 test: pin BeforeRouter to the moment before the engine exists
buildRouter is split out of run() so the order it establishes can be asserted:
the phase is announced, then initRouter builds the engine, then runStartupHooks
drains the registries.

The test covers both halves of the distinction the contract draws. A
BeforeRouter callback sees no engine - that is what the phase means, the last
point at which a module can still affect how routes are built. A callback in the
before registry, two lines later, sees one. The names invite treating them as
the same moment and they are not.

No database is involved. AuthInit reads ApplicationConfig.Mode and JwtConfig and
nothing else, and building a router registers handlers rather than calling them,
so the whole sequence runs in a package test with two package-level values set.

freshRuntime swaps the global runtime for the duration: runStartupHooks seals
the registries it drains, and a sealed registry silently drops everything
registered afterwards, which would leave every later test in this binary passing
while proving nothing.

The counter-proof compiles and fails - announcing the phase after initRouter
reports "BeforeRouter saw engine &{...}, want nil".
2026-09-05 22:31:12 +08:00
zhangwenjian dff0e64f51 test: run the queue's ordering rule against a real redis in CI
The rule that consumers are registered before the queue is started had no test
that could fail on the backend it exists for. Everything so far ran on the
memory queue, which is the default: queue.Memory's Register starts another
consumer goroutine whatever the state, so the wrong order passes there. A suite
that only exercises the default reports success for a queue that accepts no
consumers at all.

CI gets a redis service, and two tests build the queue the way setupQueue does -
through config.QueueConfig.Setup, so what is under test is the adapter this
repository actually gets, LegacyQueueAdapter included. They skip without
GO_ADMIN_TEST_REDIS_ADDR, so a developer with no server still gets a green run.

Registering first and starting second delivers the message. Starting first and
registering second is refused: no consumer group was created, so Append comes
back with storage.ErrNoHandler. Pinning that particular error rather than "some
error" is deliberate - the test is about the missing consumer, and a connection
failure that happened to error too would otherwise pass for it.

Running it corrected something written two commits ago. The claim there was
that a late registration loses consumers "with nothing said". Only half of that
holds: the registration is silent, because Register returns nothing, but every
publish afterwards fails loudly - ErrNoHandler, logged at error level by both
call sites in common/middleware - while the log rows are never written. The
symptom is missing rows plus a lot of noise, not a quiet nothing. That commit's
message and the contract doc both say so now.
2026-09-05 22:31:12 +08:00
zhangwenjian 0b78bc1e2e docs📝: write down where the life-cycle phases land in this host
core's contract says what the four phases promise. This says which line of
cmd/api/server.go each of them is, which is the part an application author
cannot read out of core.

Three things are recorded because getting them wrong is silent:

  - BeforeRouter is not the before registry. Those callbacks run from
    runStartupHooks, which is after initRouter has built the engine; the phase
    is before it. The two are one line apart in the same function and describing
    them as equivalent is the mistake this paragraph exists to prevent.
  - AfterResource runs again after every configuration reload, so a callback
    there is idempotent with respect to a resource rather than doing nothing
    the second time. The queue consumers are the worked example, in both
    directions: a new adapter must get consumers, the same adapter must not get
    them twice. Identity has to come from where the resource is created -
    GetQueueAdapter and GetQueuePrefix build a fresh wrapper per call, so two
    of them never compare equal however often the adapter underneath changed.
  - Consumers must be registered before the queue is started, and which
    implementation is behind the interface decides whether that matters:
    QueueConfig.Setup hands back queue.NewMemory when there is no redis
    section, and that one does not care; a redis section reaches
    LegacyQueueAdapter, whose Register cannot report a refusal. The table says
    so rather than leaving "only on redis" as a claim. What is silent is the
    registration alone - every publish afterwards is refused with ErrNoHandler
    and logged at error level, so the symptom is missing log rows plus a lot of
    noise, not a quiet nothing.

The third-layer table loses its "queue consumers are lost after a hot reload"
row, which is what AfterResource is for, and gains the honest replacement: the
four phases are the only mount points there are. There is no "after the routes
are installed, before the socket is listening".

AfterListen is described as the port being bound rather than Serve being in its
accept loop. Serve runs on another goroutine and may not have reached the first
Accept; what is true is that the bind returned, so the kernel is queueing
connections. A bind that fails produces no phase at all - the error returns
from run() and the banner never prints.
2026-09-05 22:31:12 +08:00
zhangwenjian 94163f9afb fix🐛: stop the job scheduler on the way out, and start one per tenant
The per-tenant setup ended with `defer crontab.Stop()` on the line above
`select {}`. The select never returned, so the deferred call was unreachable
for the life of the process: the scheduler had never once been stopped. And
because setup never returned, the `for k, db := range dbs` loop in Setup never
reached its second iteration - with several tenant databases configured, only
whichever one came first out of the map ever got a scheduler at all.

Both fall out of deleting the select, which was blocking for nothing: cron.Start
is `go c.run()` and has never needed anything to hold the caller.

The stop becomes a BeforeExit callback. cron.Stop returns a context that closes
once the jobs already running have finished, so the shutdown budget has
something real to bound - and giving up on that wait leaves those jobs running
until the process exits, which is better than holding the whole shutdown open
for one job that will not end.

Startup moves from a bare goroutine in run() onto AfterListen. Two reasons: the
phase runs behind core's panic guard, which does not reach across a goroutine
boundary, so a panic while loading jobs used to take the process down; and the
jobs it starts can call the API, which is only true once the socket is
accepting. It can be synchronous now precisely because setup returns.

Tested where it can be: startCrontab is split out so a scheduler can be started
with no database in sight. The job runs every second; after RunShutdown, two
and a half seconds of silence is the assertion. The counter-proof - registering
no callback, which is what this commit replaces - compiles and reports "the job
fired 2 more times after shutdown".

There is one test, not several, because BeforeExit closes to further
registration once it has run; a second RunShutdown in the same binary would
find an empty registry and pass while proving nothing.

**The multi-tenant half has no test.** setup needs a *gorm.DB per tenant before
it reaches the line that was blocking, and this repository's CI has no database
- `make build` is CGO_ENABLED=0 with no sqlite tag. It is the same defect
though: the loop could not advance past a call that never returned.
2026-09-05 22:31:12 +08:00
zhangwenjian 4510b06959 fix🐛: register the queue consumers before the queue is started
setupQueue ended with `go queueAdapter.Run()`, and the three log consumers were
registered afterwards, from setup(). The contract implementations refuse a
registration once the queue is running - memqueue and the redis queue both
answer storage.ErrQueueAlreadyStarted - and Register cannot report it: it
returns nothing, which its own comment in core records as the reason the
interface is deprecated. Start first and register second, across two
goroutines, and the registration is dropped without the caller being able to
tell.

What follows is not quiet. No consumer group was created, so redis refuses
every later publish with storage.ErrNoHandler, and go-admin logs that at error
level from both call sites while the login and operation log rows are simply
never written. The silence is in the registration; the cost shows up on every
request after it.

Which implementation is behind the interface depends on the configuration.
config.QueueConfig.Setup returns queue.NewMemory directly when there is no
redis section - and that one does not care about the order, because its
Register just starts another consumer goroutine. Only a redis section reaches
storage.LegacyQueueAdapter, which wraps the contract implementation and
therefore refuses. So the defect is invisible in the default deployment and
shows up only where redis is configured, dropping the login log, the operation
log and the api check - the three things #892 was about.

The start therefore moves to the code that registers, and nothing starts the
queue but that.

The registration also moves onto AfterResource. It has to: a reload rebuilds
the adapter, and consumers attached to the one that existed at start-up are
attached to a queue nobody publishes to any more. Being on that phase means
running again on every reload, so the callback is idempotent with respect to a
given queue rather than "does nothing the second time" - registering twice on
the same queue would give every message two consumers and write every log row
twice.

Identity for that comes from common/storage, where the adapter is built, as a
generation counter. It cannot come from the accessors: GetQueueAdapter and
GetQueuePrefix build a fresh runtime.Queue wrapper on every call, so comparing
two of them compares two wrappers and never matches however many times the
adapter underneath has been replaced. A counter also keeps the comparison on a
uint64 rather than an `==` between two interface values, which would panic on
an adapter type that is not comparable.

Generation 0 means the configuration has no queue section, so nothing was
installed and the runtime hands back its own memory queue. That case still gets
consumers, because the registration this replaces was unconditional and
dropping it would stop the logs for anyone who commented the section out.

Two things fixed on the way past:

  - `if q := sdk.Runtime.GetQueueAdapter(); q != nil { q.Shutdown() }` was
    always true. GetQueueAdapter never returns nil - with nothing configured
    the runtime falls back to its own memory queue and wraps that - so the
    first start shut down the fallback queue before anything had used it. Only
    an adapter this package installed is shut down now.
  - config.Setup becomes bootstrap.SetupConfig, which is what announces
    AfterResource, and announces it after the callbacks that build the
    resources rather than before.

attachConsumersOnce is split out so the order and the once-ness can be checked
against a queue the test controls; neither can be read back out of a real
adapter. Four tests cover the ordering, both directions of the idempotency
rule, and the unconfigured case. Both counter-proofs compile and fail: calling
Run before the registrations reports each of the three as "came after Run", and
dropping the generation guard reports eight calls where four are wanted.

One honest limit: the counter-proof for the ordering makes Run synchronous.
The original arrangement started the queue on another goroutine, and a race
cannot be made to fail every time - which is the reason the order is enforced
by structure here instead of being left to be noticed in use.
2026-09-05 22:31:12 +08:00
zhangwenjian 750c7c744e feat: run the BeforeExit callbacks on the way out
A module can now register cleanup and have it happen. Until this commit the
process stopped serving and returned; anything a module had set up went down
with the process rather than being taken down.

BeginShutdown is said first, before anything is dismantled. Without it a
configuration reload arriving in this window re-runs AfterResource - rebuilding
the pool and the queue adapter and re-registering consumers - on top of cleanup
that has already run.

The cleanup runs whether or not Shutdown reported an error, which is the whole
reason that error stopped being fatal in the first place: Shutdown fails exactly
when connections were still in flight, and that is when there is most left to
take down.

The two budgets are spent one after the other, so what has to fit inside the
orchestrator's grace period is their sum. `docker stop` allows 10s by default
before SIGKILL; 5+3 leaves room to finish returning. Raising one without
lowering the other buys nothing.

Both halves are tested through the existing subprocess child, which now
registers a BeforeExit callback of its own:

  - after a Shutdown that timed out, the callback still runs. Moving the call
    into the success branch reports "the BeforeExit callback did not run after
    a failed Shutdown".
  - a callback that outlasts its budget is abandoned, not awaited. It sleeps
    two seconds against a 300ms budget; RunShutdown reports the deadline, the
    process exits cleanly inside one second, and the callback's own marker
    never appears. Widening the budget to five seconds makes the test time out
    waiting for the exit, which is what "awaited" looks like.

Both counter-proofs compile and fail.
2026-09-05 22:31:12 +08:00
zhangwenjian d52dca1cb6 feat: announce BeforeRouter and AfterListen, and bind before either
Two phases are now announced from the command that serves traffic, so a module
can attach to them instead of being called by name from here.

The listener is opened by this goroutine rather than left to ListenAndServe,
which binds on the goroutine that serves. That mattered for the phase: a hook
on AfterListen is promised a reachable port, and with the bind happening out of
sight there was no way to keep that promise - "address already in use" surfaced
on a goroutine nobody read, after the banner had already announced the server
was up. It is now returned from run() and the process exits non-zero without
claiming anything.

AfterListen is announced synchronously. Moving it to a goroutine to save the
few milliseconds would let it overlap the shutdown, and on a fast SIGTERM the
cleanup callbacks could finish before the startup ones did.

What is left in the serving goroutine is still log.Fatal, deliberately: the
bind is no longer among the errors that reach it, so what remains is a serve
that failed after the port was taken, and carrying on would park the process on
<-quit with nothing serving. ServeTLS is the one case that can still fail
immediately, since it reads the certificate files - with ssl enabled a hook can
still run against a server on its way down. That is not a regression (the old
code printed the banner in the same situation) and it is not fixed here.

BeforeRouter is placed before initRouter, which is a different moment from the
before registry runStartupHooks drains: those callbacks run after the engine
has been built, not before it.

AfterListen is tested here, in one test rather than two because the phase seals
itself once it has run: a second test would find a closed registry and pass
while proving nothing. Both counter-proofs compile and fail - announcing on a
failed bind reports "AfterListen ran 1 times after a failed bind", and
`go RunPhase(...)` reports "ran 0 times, want 1" against the hook's own pause.

BeforeRouter's placement is not asserted in this commit. The test for it comes
with the buildRouter extraction later in this branch.
2026-09-05 22:31:11 +08:00
zhangwenjian 71413a4248 build🔧: require go-admin-core v2.6.0
It carries the life-cycle phases and the shutdown registry. Nothing here uses
them yet - the wiring is the commits that follow, and keeping the bump on its
own means a bisect can tell "the dependency moved" apart from "the host
started calling into it".
2026-09-05 22:31:11 +08:00
wenjianzhang 4fede43254 Merge pull request #905 from go-admin-team/fix/checksilent-datascope-route
feat(checksilent): 抓「handler 读数据权限、路由却没挂中间件」
2026-09-05 21:56:08 +08:00
zhangwenjian 0a629e2f3f docs📝(checksilent): describe the two passes the code actually makes
The comment claimed bindings were collected as the body was walked so that a
registration only saw definitions above it. That is the single-pass design this
started as. The code does two passes - one to collect, one to report - and a
registration therefore sees every binding in the function.

That is the point rather than an accident: a `.Use` written below a route is
still part of the chain, because the chain is assembled before anything is
served. The price is that a name reused for two different things in one
function resolves to the last assignment, which the comment now says instead of
promising an ordering the code does not keep.
2026-09-05 21:51:51 +08:00
wenjianzhang 37065fb089 Merge pull request #904 from go-admin-team/fix/getinfo-data-permission
fix🐛: 开了数据权限就登不进去——/getinfo 拿不到它要的 DataPermission
2026-09-05 21:34:13 +08:00
zhangwenjian 6966f14dd4 feat(checksilent): report a route whose handler reads a data permission nobody supplies
GetPermissionFromContext cannot fail. When no middleware put a *DataPermission
in the context it returns the zero value, and the zero value's DataScope is the
empty string - which is not one of the five scopes Permission recognises, so it
takes the default branch and fails closed. The query is handed `1 = 0` and
matches nothing.

The endpoint then reports "not found" or "no permission" for rows that plainly
exist, and only where enabledp is true. With data permissions off - the
repository default - Permission returns the query untouched and the missing
middleware costs nothing at all. A test suite and a CI that run on the default
cannot see it.

That is what happened to /api/v1/getinfo: it read the permission on a group
carrying only the JWT middleware, so every login on a deployment with data
permissions enabled ended in a 401 from the endpoint the browser calls
immediately after signing in, and went back to the login page. Three /sys-api
routes had the same shape.

The check matches a handler to the group it is registered on, through the AST
rather than through the text - a scratch grep for the same thing reported four
false positives from a comment that happened to contain the function's name,
and before that, a dozen from matching handler names across packages. Handlers
are keyed by package, type and method, so two SysUser types are two handlers.
Subgroups inherit their parent's chain, as gin does, and a `.Use` written below
a registration still counts, because the chain is assembled before anything is
served.

Either half is a fix and the message says both, because which one is right
depends on the route. A handler reading other people's rows wants the
middleware. A handler reading the caller's own row - id from the token - wants
no scope at all: DataScopeSelf matches on create_by, so scoping a self-read
rejects every user who did not create their own account. Reporting only "add
the middleware" would have turned /getinfo from broken into worse.

Five tests: the mistake, both fixes, subgroup inheritance, and a same-named
handler in another package. TestThisRepositoryIsClean covers the real tree, and
it is what fails on the commit before this one - four findings, all real.
2026-09-05 21:31:45 +08:00
zhangwenjian 22716e90c1 fix🐛: /getinfo cannot be scoped by a data permission it never receives
Logging in on a deployment with enabledp: true ends on the login page. The
login itself succeeds - sys_login_log records it - and then /api/v1/getinfo
answers 401 "登录失败", which sends the browser straight back.

The query behind it reads:

  SELECT * FROM sys_user WHERE sys_user.user_id = 1 AND 1 = 0 AND deleted_at = 0

The 1 = 0 comes from the data-permission scope. GetInfo asked for a permission
with GetPermissionFromContext, but the group this route sits in installs only
the JWT middleware - no PermissionAction - so nothing ever put one in the
context and what came back was the zero value. An unset scope is not one of the
five recognised ones, and since unknown scopes began failing closed rather than
silently matching every row, that zero value now means "match nothing".

The route was working by accident before, and only on deployments that enable
data permissions: the repository default is enabledp: false, where Permission
returns the query untouched. That is why the local suite and CI are both green
and the demo site is not.

Two different faults, so two different fixes:

/getinfo reads the caller's own row - the id comes from the token. A data
scope answers "whose rows may this user see", so there is nothing left for it
to restrict, and applying one is not a stricter version of the query but a
broken one: DataScopeSelf matches on create_by, and an account is created by
whoever added it, so a scoped self-read would 401 every user who did not create
their own account. It now goes through GetSelf, which does no scoping at all -
which is how GetProfile has always read the same row.

/sys-api is the opposite case. Its three handlers do read the permission, and
they are listing and updating other people's rows, so the middleware belongs
there and was simply missing. Added.

Those four endpoints were found by checking every handler that reads the
permission against the group it is registered on. The check reports four before
this commit and none after.

No test. Both paths need a *gorm.DB with sys_user and sys_role rows before they
reach the line that matters, and this repository's CI has no database - `make
build` is CGO_ENABLED=0 with no sqlite tag. What can be tested is the shape of
the mistake rather than its effect, and that belongs in tools/checksilent as a
rule of its own; it is not in this commit because a site that cannot be logged
into should not wait for it.
2026-09-05 21:24:25 +08:00
wenjianzhang 73cce7fc2f Merge pull request #903 from go-admin-team/feat/005-sigterm
fix🐛: SIGTERM 从未被处理,优雅关闭在容器里是死代码
2026-09-05 18:00:58 +08:00
zhangwenjian b59c7f0d46 test: wait for the accept, not just the dial
Moving the dial to just before the shutdown removed one flake and introduced
another: Shutdown only waits for connections the server has already accepted,
so calling it in the gap between the dial and the accept finds nothing to
wait for and returns cleanly. The test then fails on its own "this proves
nothing" guard - which it did, after passing once.

A ConnState hook closes both gaps deterministically. The connection is opened
late enough not to age past the five seconds net/http stops counting it at,
and the child does not proceed until the server has taken it off the
listener.

Ran five times in a row rather than once, because a single green run is what
made the previous version look fixed.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 16:44:53 +08:00
zhangwenjian d3a44a2a6b test: dial the stalling connection after the signal, not at start-up
net/http stops counting a StateNew connection against Shutdown once it is
more than five seconds old. The connection was opened when the child started
and the parent then waited for readiness before signalling, so on a slow run
the connection could age past that mark and Shutdown would succeed - and the
test would fail on its own "this proves nothing" guard rather than on the
behaviour it is there to pin.

Opening it immediately before the shutdown keeps the timeout deterministic.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 16:42:47 +08:00
zhangwenjian 5c3c3907d5 fix🐛: arm the stop signals before announcing readiness
The previous commit split arming from waiting so a caller could arm first,
wrote a comment saying a signal landing in between reaches the default
handler and kills the process, used it that way in the subprocess test - and
then left run() calling the combined helper after the whole readiness banner.
The window it warned about was still there in the one place that ships.

The signals are now armed before the server starts serving, and the wait
happens where it did. The disposition is restored right after the first
signal rather than deferred, so a shutdown that hangs can still be
interrupted by a second one.

waitForStopSignal goes away: run() was its only caller, and what was worth
keeping from its comment is now on armStopSignals.

Note that no test covers this ordering. The subprocess test drives
armStopSignals directly, which is what makes it a test of the mechanism
rather than of run(); moving the call back below the banner leaves it green.
Verified by reading the sequence in run(), not by a failing test.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 16:42:44 +08:00
zhangwenjian f2215e132e fix🐛: handle SIGTERM, and stop exiting on a failed Shutdown
Three defects on one path, none of which could be seen from the code alone.

**SIGTERM was never registered.** signal.Notify listened for os.Interrupt
only, and Go terminates the process outright for a signal nothing handles.
`docker stop`, a Kubernetes pod deletion and `systemctl stop` all send
SIGTERM, so every line of the graceful shutdown below the wait was dead code
outside a terminal: measured on a real binary, SIGINT printed "Shutdown
Server ..." and "Server exiting" and SIGTERM printed neither.

**A stuck shutdown could not be interrupted.** quit is buffered and
signal.Notify stays armed after the first delivery, so further signals only
refill the buffer. That was harmless while SIGTERM went to the default
handler - it was the escape hatch. Registering it removes the hatch, so the
disposition is now restored once the first signal is taken, and a second one
kills the process the default way. Arming is split from waiting so a caller
can arm before it announces readiness; a signal in between reaches the
default handler, which is the very failure being fixed.

**A failed Shutdown skipped everything after it.** log.Fatal is an
unconditional os.Exit(1), and Shutdown reports an error precisely when
connections were still in flight - the moment the cleanup that follows
matters most. It is an error now, and the process carries on.

That failure is closer than it looks. net/http only treats a StateNew
connection as idle once it is over five seconds old, so a connection opened
shortly before the signal that has sent nothing holds the whole budget: with
the shipped settings.yml (readtimeout 1) the server closes it first and
shutdown takes 5ms, but with settings.demo.yml (readtimeout 10000) the same
connection made shutdown take 5.04s and exit 1, printing no "Server
exiting". The default configuration is what has been hiding this.

The wait and the shutdown are extracted so the subprocess tests can drive the
real functions against an empty http.Server: CI has no database, and none of
this needs one.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 15:28:15 +08:00
wenjianzhang a69afab34f Merge pull request #902 from go-admin-team/feat/006-contract-docs
docs📝: 契约文档指向 core,而不是宿主
2026-09-05 11:29:01 +08:00
zhangwenjian e0132db1b9 docs📝(checksilent): retire a comment that predates the lowering
The note explained the empty-shim summary as the expected state "until the
contract packages are lowered into core". They are lowered, and the shims
exist - so a count of zero now means they stopped being aliases, or stopped
being here, which is the interesting case rather than the ordinary one.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 11:23:31 +08:00
zhangwenjian 7c3f55a873 fix🔧(checksilent): suggest the fix with the qualifier the file uses
The contract-shim-alias message built its suggested line from path.Base of
the import path, so it told the author to write

    type ControlBy = models.ControlBy

in a file whose import is `contractmodels "…/sdk/contract/models"`. Every
shim in this repository aliases that import, so the suggestion never
compiled as written - in the one message whose whole job is to be pasted in.

qualifiedType already read the in-source identifier to resolve the import;
it now returns it.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 11:23:31 +08:00
zhangwenjian f7c0247394 fix🔧(checksilent): stop the seeded-value checks reporting their own tests
Widening menu-sort-overflow to see a contract MenuSpec made it fire on
app/admin/service/seed_test.go, on the case that asserts SeedMenus rejects
a sort of 900. The check was reading the proof that it works as a defect.

That is not specific to this one guard: menu-sort-overflow,
config-value-truncation, menu-id-collision and modeltime-mix are all about
a value that reaches a real database through a migration, a test fixture
reaches none, and every one of those guards needs a test that writes the
value it rejects. Skip _test.go in all four.

The two import and alias checks keep scanning tests - those are about the
dependency graph, where a test file's import is as real as any other, and
TestContractImportBoundaryCoversTestFiles already pins that.

Both directions are covered: a fixture is ignored, a real seed is still
reported.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 11:16:53 +08:00
wenjianzhang ac23556029 Merge pull request #900 from go-admin-team/feat/006-host-wiring
fix🐛: 契约注册面接上宿主的执行端
2026-09-05 11:14:01 +08:00
zhangwenjian f64115e03a docs📝: state what an off-convention migration file name does
The naming rule was documented; what happens when it is broken was not.
It now panics naming the offending file, which is worth saying out loud
because the alternative it replaced was silent: a name that is not a
timestamp used to register as its own version, and that migration would
never run and never report anything.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 11:14:01 +08:00
zhangwenjian a2524c31bf docs📝(contract): state the two menu seed rules a caller cannot infer
Sort has an upper bound: sys_menu.sort is built as a tinyint, sqlite ignores
the width, and an overflow surfaces as Error 1264 partway through a migration
rather than as a rejected value.

MenuSpec carries no menu name, and the host synthesises one from the app code
and the spec code rather than using Code directly - two applications both
choosing "list" would otherwise share a keep-alive cache key on the frontend.
Nothing in the type says so, and every Seeder implementer would have to
rediscover it.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 11:14:01 +08:00
zhangwenjian 71d6211c61 fix🔧(checksilent): see a menu written as a contract MenuSpec
The sort-overflow check recognised a SysMenu literal from this repository's
model packages and nothing else. An application installed from outside cannot
reach that type - it describes the same row as a seed.MenuSpec and hands it to
the host's Seeder - so the check went quiet for exactly the author furthest
from the schema it protects.

Not hypothetical: this repository's own reference application shipped a Sort
of 200, past the tinyint sys_menu.sort is built as, and this check passed it.
sqlite ignores the width, so it would have surfaced first on a real install,
as Error 1264 partway through a migration with everything after it unapplied.

The check still cannot see an application in the module cache; that half is
the Seeder's runtime validation. This closes the half that is in the tree.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 11:14:01 +08:00
zhangwenjian c67760bc39 docs📝(agents): match the contract rules the reference document now states
Two of the three bullets on the contract surface disagreed with
docs/contract.md and with core's own. Registration is constrained by
ordering - it must happen before the startup hooks run - not by being
written inside `init()`; and the claim that core's setters take no lock
is not true of them. The check table gains the new alias rule.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 11:14:01 +08:00
zhangwenjian d3e7f46a46 docs📝(contract): point applications at core, not at the host
The list of stable packages named four packages of this repository, on
the stated grounds that deduplicating app/demo's imports produces
exactly those four. That reasoning was wrong in the one direction that
matters: it sends a third-party author to depend on the host, and the
host is a fork that every user edits. `go-admin` is also not a
resolvable module path - it has no dot in its first element - so an
application cannot require it at all without a replace directive, which
is ignored outside the main module.

Rewritten around what core promises instead, and around a different
question: not "which packages does an application import" but "which
conventions fail without saying anything". Those are now spelled out
one by one, each with the mechanism that makes it silent - the response
envelope the frontend reads by `code`, the tenant-scoped connection,
`create_by` and the soft-delete marker, the data-scope middleware, the
transaction shape, and the `apps/` prefix a packaged application's menu
component must carry.

Also states two things the document was missing: installing an
application means trusting it with the host's database connection, at
the same level of trust as importing any other Go package - there is no
sandbox here and this does not pretend otherwise - and wiring an
application in touches two places, not one, where missing the second
means the migrations simply do not run, with no error.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 11:14:01 +08:00
zhangwenjian 55866682ae feat(checksilent): require a contract shim to be a type alias
A shim of a core contract type written as `type X pkg.Y` instead of
`type X = pkg.Y` keeps the fields and drops the method set, so anything
embedding it stops satisfying the interfaces it satisfied before.

The compiler catches that only where the method set is actually
exercised. This repository exercises some of the contract types through
an interface and some not at all, so the ones it does not exercise
compile here and break in a fork or a third-party application - which
is the half nobody is watching.

The trigger is the right-hand side of the declaration rather than a
list of package names, so it covers whatever the lowering ends up
shaping without a list to keep in step. Until the contract packages
land in core there is nothing here to guard, and the summary says so
rather than letting the silence read as a pass.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 11:14:01 +08:00
zhangwenjian 550e95ff43 docs📝: say which way sys_menu.visible points
The comment called Visible "0" "hidden by default" and then said an
administrator should not have to unhide the menu - which cannot both be
true. "0" is shown; every menu this repository seeds, including the demo
product menu that is visible on the demo site, uses it.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 11:08:02 +08:00
zhangwenjian 060b6cfd64 fix🐛: grant an application's apis even when it registers no menus
grantToAdminRole does two independent things - it grants the menus to the
admin role and writes a casbin rule per api - and SeedMenus skipped the
whole call whenever the menu list came back empty.

An application is free to register apis with no menus: endpoints another
service calls, a webhook, a UI mounted somewhere else. Those installs wrote
their sys_api rows and then no casbin rule for any of them, so every one of
those endpoints was denied to everyone, admin included - from a migration
that reported success and left rows in the table to prove it had run. There
is nothing to look at afterwards that says what went wrong.

Guard on both lists instead, so nothing registered stays a no-op and apis
alone still get granted.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 11:08:02 +08:00
wenjianzhang 89a4738394 Merge pull request #901 from go-admin-team/feat/006-example
feat: example/app-order —— 只依赖 core 的参照应用
2026-09-05 10:58:40 +08:00
zhangwenjian d8529289cf fix🐛: fold the host's GetFilename into the contract's
The host kept its own copy of the version-naming rule, byte-identical to
the one in contract/migration: slice the leading 13 characters, no check.
Two copies of a convention that applications also have to follow is two
things to keep in step, and the copies had already stopped matching - core
now rejects a name that carries no timestamp, and this one still accepted
"add_orders.go" and registered a migration under that string as its
version, which nothing would ever match and nothing would report.

Delegate instead, so there is one implementation of the rule and an app's
migration and a host migration derive their version the same way.

The test pins the reject case, not just the happy path: a re-divergence
that only sliced would still pass the happy path.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:58:39 +08:00
zhangwenjian 4a8f97b1ee feat: implement the menu seeder an application registers against
core's seed package defines what an application may ask for and leaves the
writing to the host, which is the only side that knows its own tables. No
host implemented it, so SeedMenus returned ErrNoSeeder and an application's
menus never appeared.

adminSeeder writes all four kinds of row, not the two an obvious reading
would stop at: without sys_menu_api_rule and the sys_role_menu / casbin_rule
grants, the menu exists and no role can reach it.

Ids are always autoincrement, never caller-assigned - checksilent's
menu-id-collision check reads literals in this repository's tree and cannot
see an application in the module cache, so the collision is removed by
construction instead of guarded. The runtime validation covers what a static
scan cannot reach for a third-party spec: duplicate codes, unresolved parents
and api references, an unknown kind, and a sort outside sys_menu.sort's
tinyint range.

MenuSpec carries no menu name, so one is synthesised from the app code and
the spec code - two applications both choosing "list" would otherwise collide
on the frontend's keep-alive key.

It lives in app/admin/service because cmd links both subcommands into one
binary, so its init runs whichever one is invoked, and cmd/migrate never has
to import app/admin to reach it.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:58:39 +08:00
zhangwenjian d54ac844ef feat: record which application a menu row and an api row came from
sys_migration already carries app_code; sys_menu and sys_api did not, so
nothing said which application seeded a row - which is what an uninstall or
an audit would have to ask.

The migration adds the columns through the runtime models rather than
cmd/migrate/migration/models, whose frozen ModelTime is wrong for anything
ordered after the soft-delete conversion.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:58:39 +08:00
zhangwenjian 379fba515f fix🐛: run the migrations a third-party application registers
core's sdk/contract/migration keeps its own process-wide registry, because
that is the only door open to an application that must not import the host.
Nothing here ever opened it: ForApp("crm").SetVersion(...) compiled,
registered, and then never ran - no error, no mention in status, nothing.

mergedEntries unions the host's own registry with contract/migration's
Snapshot(), and status, run and AppCodes all read through it, so migrate,
status, --dry-run and --app see an application's migrations exactly as they
see the host's. Version namespacing already keeps the two apart, so a key
collision should not be reachable; the host's own registration wins if one
ever is, rather than being silently replaced.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:58:39 +08:00
zhangwenjian 58105cb478 docs📝(example): drop a stale ordering claim from the router test
The comment said the test had to be declared first because Go runs a
package's tests in source order. That is not a guarantee, and it is not what
makes this work: the test that registers RoleCheck puts it back in a
t.Cleanup, and the guard here turns a wrong order into a loud failure rather
than a silent pass. Verified with go test -shuffle on seeds that run the two
in either order.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:53:48 +08:00
zhangwenjian 47af6f4306 fix🐛(example): make the swagger annotations resolve
The Create handler's @Param named dto.OrderCreateReq, but this file imports
that package as orderdto. swag stops on it:

    ParseComment error ... cannot find type definition: dto.OrderCreateReq

The @Success annotations name models.Response, which resolves - through
--parseDependency - to core's sdk/contract/models.Response rather than to
this package. That is the right envelope, and worth a note next to the
import, because the obvious "correction" is wrong: core's response.Response,
which the framework's own handlers name, carries no data field, so switching
to it would document these endpoints as returning no payload.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:53:45 +08:00
wenjianzhang 7d29c9953a Merge pull request #899 from go-admin-team/feat/006-jwt-hoist
fix🐛: JWT 中间件注册成了取不出来的形状
2026-09-05 10:31:44 +08:00
zhangwenjian 0729624c2f fix🐛(example): bring the directory menu's sort inside a tinyint
sys_menu.sort is `gorm:"size:4"`, which MySQL builds as a tinyint holding
-128..127. Sort: 200 passes every sqlite test - sqlite ignores the width -
and fails on a real install with Error 1264, partway through a migration.

This is the exact incident class checksilent's menu-sort-overflow check
exists to prevent, and it reached a hand-written deliverable anyway: that
check only recognises a SysMenu literal from the host's model packages, so
a seed.MenuSpec is invisible to it. Widening the check is tracked
separately; this is the value it would have caught.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:27:42 +08:00
zhangwenjian b3a740ab2a feat(example): register the order routes, migration and menu seed
Registration goes through core's package-level facades: SetAppRouters for
the routes, migration.ForApp for the schema, and seed.MenuSpec/ApiSpec
for the menu rows - none of which requires importing the host.

The menu component is spelled apps/order/order/index. The frontend tells
a packaged view from a built-in one by that first segment alone, and
getting it wrong is silent: the page falls back to the not-installed
placeholder while the console names a src/views path that was never going
to exist. The tests assert that prefix, that every Parent reference
closes, and that every ApiCode resolves - the three ways a menu graph is
wrong without anything saying so.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:27:41 +08:00
zhangwenjian adf617f5d0 feat(example): hand-write the order service and api layers
No generic CRUD action anywhere: real business - a cross-table order
placement, a payment transition - is what the contract surface has to
carry, and the actions cover only the single-table case that a real
application outgrows immediately.

The transaction is Orm.Transaction(), not the Begin/defer shape that
app/admin/service/sys_role.go and three other files use. That shape
commits a half-written transaction when the body panics, because the
deferred check reads err, which a panic leaves nil.

The payment transition guards concurrency through the update itself -
WHERE status = 'pending' plus RowsAffected - rather than a read followed
by a write.

The tests cover both rollback paths, because they fail differently: a
mid-transaction error returns, a panic unwinds - and the second is what
tells Orm.Transaction() apart from the shape it replaces. The concurrency
test pins the pool to one writer so sqlite's own single-writer semantics
cannot stand in for the guard being tested. The data-scope tests assert
the fail-closed direction too: an unrecognised scope must return no rows
rather than every row.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:27:41 +08:00
zhangwenjian 049a20cd04 feat(example): add the order example's models
A reference application for a third-party author: its own module, and a
require list that names go-admin-core and nothing else. The point of the
example is that constraint - an application that reaches for the host
cannot be installed through a module proxy at all, because `go-admin` has
no dot in its first path element and a replace directive is ignored
outside the main module.

Two tables rather than one, because a single-table example proves only
what the generic CRUD actions already proved. The interesting question is
whether the contract surface holds up for business that spans tables.

The table names carry an app_ prefix: "order" is a reserved word, and
Permission() interpolates the table name into raw SQL without quoting.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:27:41 +08:00
zhangwenjian be3c4452e3 test: pin the jwt handler being retrievable and shared
Two properties the previous shape broke silently: GetHandlerFunc must
report ok for the JwtToken key, and every module must read back the same
instance. Reverting the registration to the unbound method expression
still compiles and turns the first of these red, which is the failure
this pins.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:26:54 +08:00
zhangwenjian 5b01c9ada8 fix🐛: build the shared jwt middleware instance once in InitMiddleware
Four modules each called AuthInit and built their own instance, so which
one Runtime handed back was decided by whichever module initialised last.
The JwtToken key was also registered as an unbound method expression,
which GetHandlerFunc's type assertion can never match - the key was
registered and unusable at the same time.

The instance is now built once here and registered as a bound closure.
Modules read it back through GetAuthMiddleware, which is fatal rather
than nil when called before InitMiddleware has run: a process without a
JWT middleware should not reach the point of serving a request.

Only one call site needs the instance itself rather than the handler
(admin's /login, for LoginHandler); the thirty-odd MiddlewareFunc() call
sites are unchanged.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:26:54 +08:00
wenjianzhang ffd82a6a10 Merge pull request #898 from go-admin-team/feat/006-shim
refactor🎨: 契约包改为 core 的薄壳
2026-09-05 10:26:52 +08:00
zhangwenjian dd8d89a990 test: make the index probe return a copy, like every real dto.Index does
IndexAction closes over one dto.Index and serves every request to the route
from it; Generate exists so each request gets its own instance, and every
implementation in this repository returns a copy for that reason. The probe
returned the receiver, which made it the one shape IndexAction is not
written against - and inconsistent with probeRow in the same file, which
already copied.

A single-request test cannot tell the two apart, so the assertion is on
Generate itself rather than on the action's behaviour.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:22:12 +08:00
zhangwenjian 2e5b23565e test: cover data permission through a real CRUD action
create.go/delete.go/index.go/update.go/view.go were not lowered to
core (PRD 006 F3) and still call actions.Permission directly, in this
repository, on a code path core's own test suite knows nothing about:
core pins down what Permission builds for a given scope, but nothing
covered whether this package's five Actions still remember to call it
at all. TestIndexActionAppliesDataPermission runs IndexAction exactly
as a real request would, against a real in-memory database, and
inspects the SQL GORM actually executed - not just that the handler
returned success, which it would just as happily do with the filter
missing entirely.

The SQL is captured through a gorm.io/gorm/logger.Interface wrapper
rather than read back from IndexAction's own *gorm.DB: IndexAction
builds and executes its query in one unbroken chain
(Model().Scopes().Find()...Count()) and never hands the built
statement back to its caller, so there is nothing else to inspect it
through.

Counterproof performed and reverted (not part of this commit): with
Permission(object.TableName(), p) removed from IndexAction's Scopes
call, the test failed with the captured SQL carrying no WHERE clause
at all (`SELECT * FROM action_probe_row LIMIT 10`); index.go was then
restored to its committed content (`git diff --exit-code` verified
clean).

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:15:58 +08:00
zhangwenjian f4e3f04d30 refactor🎨: turn common/actions' data permission into a thin forward
DataPermission, PermissionAction, Permission, GetPermissionFromContext,
IsValidDataScope, PermissionKey and the five DataScope* constants now
forward to go-admin-core's sdk/contract/actions, which carries the
already-fixed logic from feat/006-security-prereq (PRD 006 F14/H1-H3).
create.go/delete.go/index.go/update.go/view.go - the five generic CRUD
actions - are untouched: they call Permission and
GetPermissionFromContext by the same names, which now resolve to
forwards with identical behaviour, and stay in this package rather
than moving to core (PRD 006 F3: core's exports are a permanent
promise every fork inherits, and CRUD shape is this framework's most
volatile surface).

PermissionKey is declared as `const PermissionKey =
contractactions.PermissionKey`, a direct reference rather than a
restated literal, per PRD 006's hard constraint 4: PermissionAction
sets this gin context key and GetPermissionFromContext reads it back,
and an independently declared copy could silently drift from core's if
one were ever edited without the other. permission_test.go replaces
the detailed data-permission regression suite - which now lives in
core, next to the logic itself - with a test of this package's own
wiring: that PermissionAction and both of this package's own read
paths (GetPermissionFromContext, and c.Get(actions.PermissionKey)
directly) still meet on the same key.

Counterproof performed and reverted (not part of this commit): with
PermissionKey redeclared here as the literal "dataPermission" and
core's copy changed to a different value, TestPermissionKeyMatches-
WhatPermissionActionSets went red while GetPermissionFromContext's own
round-trip stayed green - confirming the exported constant, not the
GetPermissionFromContext wrapper, is what an independent literal would
put at risk.

PRD 006 F3/F5.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:15:58 +08:00
zhangwenjian 954ebdc9eb refactor🎨: turn common/dto into a thin alias of go-admin-core
AutoForm, ObjectById/ObjectGetReq/ObjectDeleteReq, Pagination,
GeneralDelDto/GeneralGetDto and Index/Control are now type aliases of
go-admin-core's sdk/contract/dto; OrderDest, MakeCondition and
Paginate forward to the same package (functions cannot be aliased the
way types can).

MakeCondition no longer reads common/global.Driver to choose which SQL
dialect to resolve search tags against. The lowered version reads
db.Dialector.Name() from inside the closure it returns instead, which
is always the driver the caller's own *gorm.DB is bound to - correct
even with more than one database open with different drivers, which a
single process-wide variable could never be. global.Driver is marked
Deprecated accordingly; it is still set and still readable for fork
code that reads it directly.

PRD 006 F2/F5.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:15:58 +08:00
zhangwenjian 2840010dfd refactor🎨: turn common/models into a thin alias of go-admin-core
ControlBy, Model, ModelTime, ActiveRecord, BaseUser, Response, Page,
Migration and the menu type constants now read `type X = pkg.X` /
`const X = pkg.X` against go-admin-core's sdk/contract/models instead
of defining these shapes locally. Every embed, GORM tag and JSON tag
is unchanged - a type alias is the same type, not a new one - and
every existing import of go-admin/common/models keeps compiling with
no changes of its own (verified with `git diff --exit-code` over the
70 files that import common/models, common/dto or common/actions).

The menu type constants (Directory/Menu/Button) are declared as direct
references rather than restated literals: an independently written
copy of the same value can be edited out of step with go-admin-core's,
where a direct reference cannot (PRD 006 hard constraint 4).

PRD 006 F1/F5.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:15:51 +08:00
zhangwenjian b147d9b833 build🔧(deps): require go-admin-core v2.5.0
v2.5.0 carries the sdk/contract packages the commits that follow alias
common/models, common/dto and common/actions onto.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-05 10:15:33 +08:00
wenjianzhang b3ecb81614 Merge pull request #896 from go-admin-team/fix/sys-user-privesc
fix🐛: 修复 sys-user 更新接口的垂直越权
2026-09-05 00:59:59 +08:00
wenjianzhang ce4581bb99 Merge pull request #897 from go-admin-team/feat/006-security-prereq
fix🐛: 数据权限的三处静默失效
2026-09-05 00:59:12 +08:00
zhangwenjian f406ca0160 test: fail loudly instead of skipping when the sqlite setup breaks
The privilege-escalation tests skipped themselves when opening the in-memory
database or running AutoMigrate failed. Both depend on nothing outside the
process, so a failure there means the environment is genuinely broken - and a
security regression that quietly does not run is worse than one that is
missing, because CI stays green either way.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-04 19:10:53 +08:00
zhangwenjian 39ea1f6aef fix🐛: normalize a NULL data scope too, not just an unrecognized one
`NULL NOT IN (...)` evaluates to NULL rather than true, so the previous
condition left a NULL data_scope exactly as it found it - and NULL is the one
value that most needs the repair: it scans into a Go string as "", which is
what the fail-closed default now refuses.

The column has no NOT NULL constraint, so the value is reachable from any
writer that is not the admin UI.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-04 19:10:53 +08:00
zhangwenjian b2053f507a refactor🎨: drop the unused second data permission implementation
app/admin/models/datascope.go carried a second copy of the scope logic with no
callers. Its department-tree pattern was written as "%" + id + "%" instead of
"%/" + id + "/%", so dept_id 1 also matched /11/, /21/ and /100/ - visibility
into unrelated subtrees.

It sat where someone looking for a data permission example would find it. The
copy that is actually wired up stays in common/actions.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-04 17:24:55 +08:00
zhangwenjian 9520117914 feat: normalize invalid data scopes on existing installs
The seed fix only reaches new installations. An install that imported the old
db.sql already has an administrator with an empty data_scope, and after the
fail-closed change that account sees nothing.

The migration rewrites any value outside "1".."5" to "1", which is the
behaviour those rows had before. Plain SQL rather than the frozen migration
models, per the rule that migrations after 1786700003000 must not use them.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-04 17:24:55 +08:00
zhangwenjian 7a5fc7d440 fix🐛: give the seeded admin role an explicit data scope
The shipped seed data left data_scope empty for the built-in administrator.
That was harmless while an unrecognized scope meant "see everything"; with the
previous commits it means the opposite, so a fresh install with data permission
enabled would have blinded its own default account on every list endpoint.

The admin short-circuit does not help here: role_key == "admin" bypasses Casbin,
not the data permission scopes, which never look at role_key.

The value is "1" - all data - which is the behaviour the empty string used to
produce, so this restores the intent rather than tightening it. A test reads
both seed files back so the pair cannot drift apart again.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-04 17:24:41 +08:00
zhangwenjian bd5e83d464 fix🐛: validate data scope on the role DTOs
Nothing checked what went into sys_role.data_scope, so creating a role without
a dataScope stored an empty string - the value that used to be indistinguishable
from "see everything".

All three DTOs that write the column are validated, not just the insert path:
they target the same column, and guarding one entrance while leaving two open
would not be a guard.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-04 17:24:41 +08:00
zhangwenjian 63dd40a8d7 test: cover the data scope failure directions
A table over all five scopes plus the ones that are not scopes, asserting the
generated SQL rather than a boolean, because the defect was that two different
intentions produced the same query.

The rows that matter are the negative ones: an unrecognized value, a zero
value, and a department scope with a non-positive id. Each was verified to go
red with its own fix reverted and the others in place, so a regression names
the defect it belongs to.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-04 17:24:41 +08:00
zhangwenjian 32bd88504d fix🐛: fail closed when the department id is not positive
dept_path is built as "/0/" + id + "/..." for every department, so a DeptId of
0 turns the department-tree pattern into '%/0/%' - which matches every row in
sys_dept. The scope meant to narrow visibility to one subtree returned the
whole organisation instead.

Both department scopes now refuse a non-positive id rather than building a
pattern from it. The admin DTO validates deptId, but seed scripts, SSO and
third-party registration paths do not, and after the contract move the caller
is no longer ours to control.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-04 17:24:26 +08:00
zhangwenjian d70818a9db fix🐛: fail closed on an unrecognized data scope
The switch ended in `default: return db`, which is the same answer as "this
role may see everything". That made a legitimate scope indistinguishable from a
broken one: "1" (all data) had no case of its own and fell into default too, so
"1", "", "6" and a zero value all produced byte-identical SQL.

Three changes, in this order, because reversing them would break "1":

  - the five scope values become named constants, so a reader can tell which
    string means what without consulting the seed data
  - "1" gets an explicit case, which is what frees default to mean "not a
    scope I recognise"
  - default now matches nothing rather than everything

SysRole DTOs accept the scope unvalidated, so an empty string reaches this
switch from ordinary use, not just from a corrupted row.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-04 17:24:26 +08:00
zhangwenjian 9d4a425fc0 fix🐛: abort the request when the ORM is unavailable
PermissionAction logged the error and returned. Gin treats a plain return as
"carry on", so the request reached the business handler with PermissionKey
never set - and a zero DataPermission means Permission() adds no WHERE clause
at all. A database hiccup turned into full visibility, silently.

The neighbouring newDataPermission branch already aborts. This one now does the
same.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-04 17:24:12 +08:00
zhangwenjian 4d6456a588 test: cover vertical privilege escalation on sys-user update
Two directions, because the fix has to hold both: an attacker with no policy on
this route cannot raise another user's role, and a self-edit cannot raise its
own. The second one is what keeps the fix from being "just remove the route
from CasbinExclude", which would break the profile page.

The tests drive the handler directly rather than through the router, because
the middleware is exactly what does not run for this route - the defence lives
in the handler, so that is where it has to be proven.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-04 17:23:07 +08:00
zhangwenjian 07ff92aa55 fix🐛: lock privileged fields on self-edit
The profile page posts the whole user object back, including roleId, deptId and
status, because it renders from a full SysUser it fetched earlier. A caller
editing their own record can therefore hand back a tampered roleId.

Self-edits now reload those three fields from the database and ignore whatever
the request carried. For an honest client this is a no-op - the values it sends
are already its own - so the profile page keeps working unchanged.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-04 17:23:07 +08:00
zhangwenjian 4156387eb9 fix🐛: enforce Casbin when editing another user
PUT /api/v1/sys-user sits in CasbinExclude so the profile page can reach it,
which means AuthCheckRole never runs for this route. The handler took the
target user id from the request body, so any authenticated caller could edit
another user's record - including their roleId.

The route has to stay excluded: the profile page and the admin user list share
this one endpoint, so removing the exclusion would break self-service editing
for every non-admin role. The check therefore moves into the handler: when the
target is not the caller, the request is put through Casbin explicitly.

EnforceRoleFor carries the same admin short-circuit and enforcement AuthCheckRole
uses, so a route that opts out of the middleware can still ask the same question.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-04 17:23:01 +08:00
wenjianzhang 34773a0a81 Merge pull request #894 from go-admin-team/chore/core-v2.4.1
Bump go-admin-core to v2.4.1
2026-09-01 20:47:11 +08:00
zhangwenjian 36a018400b chore🔧(deps): bump go-admin-core to v2.4.1
Documentation wording only; no code change between the two.
2026-09-01 20:42:36 +08:00
wenjianzhang 7bb02c5f1d Merge pull request #893 from go-admin-team/docs/contract-wording
Describe the rules rather than who follows them
2026-09-01 20:38:34 +08:00
zhangwenjian 15fb128236 docs📝: describe the rules rather than who follows them
The warning on Authorizator matters to anyone keeping a copy of that file, not
to one particular consumer, and it reads better addressed to all of them: check
what reads those context keys before taking this change.
2026-09-01 19:56:27 +08:00
wenjianzhang 3581e060ec Merge pull request #891 from go-admin-team/feat/003-app-prep
Groundwork for installable applications
2026-09-01 19:38:23 +08:00
zhangwenjian 0604a29596 feat(server): run the startup hooks through core
The package-level AppRouters slice keeps working and keeps running first, so
a fork that only ever appended to it sees no change. What is new is that the
core registry runs too, and that before callbacks run at all - this server
never had a loop for them.

Both go through core RunAppRouters / RunBefore, which brings the panic guard
and the seal with them.
2026-09-01 18:18:38 +08:00
zhangwenjian d8a2958797 chore🔧(deps): bump go-admin-core to v2.4.0 2026-09-01 18:18:38 +08:00
zhangwenjian ab28fa7bed docs📝: write down what a third-party app may depend on 2026-09-01 17:45:41 +08:00
zhangwenjian e88d751039 chore🔧: run the silent-failure checks in CI 2026-09-01 17:45:41 +08:00
zhangwenjian b836945eea feat: add checksilent, for the failures that do not report themselves
Six checks, five at ERROR and one - the cross-repository menu-name comparison -
at WARN, because it can only match by regular expression across two modules and
a false positive that fails CI teaches people to silence the tool.

The summary names which contract roots were actually scanned: core/ is a
separate module with no directory here, and a check that quietly covers less
than it claims is worse than no check.
2026-09-01 17:45:41 +08:00
zhangwenjian d7a8e66753 feat: add migrate status, --dry-run and --app
--app rejects a code nothing was registered under, on all three paths. It used
to take a typo as "nothing matched" and report success: migrate said the app
was unknown and still exited 0, while --dry-run and status printed the same
words an up-to-date database produces.
2026-09-01 17:45:40 +08:00
zhangwenjian 68780a845c feat: register migrations under an app code with ForApp 2026-09-01 17:45:40 +08:00
zhangwenjian 487dc94a2e feat: record on sys_migration which app a migration belongs to 2026-09-01 17:45:40 +08:00
zhangwenjian 016e977776 refactor🎨: drop Authorizator assertions that never matched
The map Authorizator receives is built by IdentityHandler in the same file and
carries IdentityKey / UserName / RoleKey / UserId / RoleIds / DataScope - not
user and role. Both assertions failed on every request, and because the ok
result was discarded the five c.Set calls stored zero values and the function
returned true anyway. Nothing in this repository or in core reads those keys.

go-admin-pro has its own copy of this file and does read them; this change
must not be carried over there verbatim.
2026-09-01 17:45:40 +08:00
zhangwenjian dcfe512204 refactor🎨: move the operation log status constants out of app/admin
common/middleware imported app/admin/service/dto for two string constants,
which made a package apps are told to build on depend on one particular app.
2026-09-01 17:45:40 +08:00
zhangwenjian fe6ebfd47c chore🔧: ignore the Go workspace files and the checksilent binary
go.work points this module at a local checkout of go-admin-core while the
two are developed together. It is a local tool and must never be committed:
CI resolves core from go.mod.

checksilent is where `go build ./tools/checksilent` drops its binary - four
megabytes beside the server one, which was already ignored by name.
2026-09-01 17:45:40 +08:00
wenjianzhang eba5fba3da Merge pull request #889 from go-admin-team/fix/password-hook-and-body-buffering
fix: a password hook that could destroy credentials, and a body copy on every request
2026-09-01 14:22:13 +08:00
zhangwenjian b7e9a79225 fix🐛: refuse an out-of-scope API update with the permission message
The previous commit added a data-permission scope to SysApi.Update and
returned early on db.Error, which left the RowsAffected check below it
unreachable: First reports a row the scope excluded as ErrRecordNotFound,
so the caller got "record not found" where the code meant to say
"无权更新该数据".

Map that one error to the permission message and drop the check it made
dead. The two cases - the row does not exist, and the row exists but is
not yours - have to look the same from outside, and now do.

Found by Copilot's review of #889.
2026-09-01 14:17:20 +08:00
wenjianzhang deffb19fd8 Merge pull request #888 from go-admin-team/ci/run-tests
Run the test suite in CI
2026-09-01 11:38:10 +08:00
zhangwenjian c858b322bd fix🐛: apply the data permission when updating an API
SysApi.Update took a DataPermission and never used it, so with data
permission enabled the update reached rows the caller could not read
through GetPage, Get or Remove, which all scope the query. It also
reported "无权更新该数据" for a row that simply did not exist, a message
that only becomes true once the scope is applied.

Drops the Debug() left on the query, which logged the statement for
every call.
2026-09-01 11:35:51 +08:00
zhangwenjian 1b5b52f0f1 perf👌: only read the request body when the operation log will store it
LoggerToFile is registered on the engine, so every POST, PUT, GET and
DELETE had its body copied into memory - through a bytes.Buffer, a
ReadAll and a string conversion - before any handler ran. The only
consumer is operParam on the operation-log row, which is written when
logger.enableddb is on, and that is off in the shipped configuration.

There was no size limit either, and a file upload is a POST like any
other: a 1MB request allocated 4.3MB here and a 16MB upload allocated
about 67MB, to build a value nobody stored.

The body is now read only when the operation log will use it, and at
most 32KB of it. The handler still receives the whole request: it reads
the copied part from memory and the rest from the connection, so what
this holds is bounded however large the request is. 32KB also keeps the
value inside the TEXT column it is written to.

The bufio.Writer this replaces was never flushed. Nothing was truncated
only because bytes.Buffer implements io.ReaderFrom, so io.Copy bypassed
the buffer entirely - a different destination would have dropped the
tail of every request body.
2026-09-01 11:35:45 +08:00
zhangwenjian ecb31a158b fix🐛: stop re-hashing a password that is already hashed
BeforeCreate and BeforeUpdate run Encrypt on whatever is in the struct,
and a user read from the database carries the stored hash in Password.
Hashing it again produces a hash of a hash: the password that user knows
stops matching, they cannot log in, and nothing reports an error.

Only the Omit("password") on SysUser.Update stood between that and the
stored credential. Any other write to this model - a profile update
written the way every other model here is written - destroys the
password, permanently and silently.

Encrypt now returns early when Password already parses as a bcrypt hash.
That also removes the round SysUser.Update was paying and discarding:
306ns where it was 54.7ms, on a route reachable without the permission
check, since PUT /api/v1/sys-user is in CasbinExclude.

The cost of deciding from the value is that a password which is itself a
well-formed bcrypt hash would be stored unchanged. That is a
60-character string beginning "$2a$", and it grants whoever set it no
access they did not already have.
2026-09-01 11:35:33 +08:00
zhangwenjian 1aecc140dc ci🔧: run the test suite on every push and pull request
The repository has 19 test files and nothing was running any of them. Both
workflows build with go build, which does not compile _test.go, the Makefile's
test target was commented out, and there is no pre-commit hook. Every test in
the tree, including the schema guards that exist precisely to catch a silent
breakage, only ran when someone remembered to type go test.

Enables the commented-out target and calls it from go.yml, the one workflow
that fires on every push and pull request. build.yml is left alone: it skips
documentation-only changes and deploys on master, so it is the wrong place for
a gate that should never be skipped.

Runs with -race. common/actions reuses model instances across concurrent
requests, so a Generate() that returns in place rather than a copy leaks data
between them, which a single-threaded run cannot see.

Verified locally: the suite passes under CGO_ENABLED=0 and under -race, and
make test exits non-zero when a test fails, so the step actually gates.

Claude-Session: https://claude.ai/code/session_01DJhM6LvhkNPej35wy9F7Aq
2026-09-01 11:33:08 +08:00
wenjianzhang e464a4aedd Merge pull request #887 from go-admin-team/fix/migration-model-soft-delete-drift
Guard new migrations against the frozen seed models
2026-08-31 15:06:05 +08:00
wenjianzhang 595c4a6be5 Merge pull request #885 from go-admin-team/fix/casbin-tenant-and-pattern-cache
fix: key the casbin enforcer by tenant, and stop recompiling patterns in the exclusion scan
2026-08-31 15:01:42 +08:00
zhangwenjian d115c5299c docs📝: say which models package a new migration may seed through
The hazard had no signal at its point of contact. Someone adding a business
module is told to copy 1786700001000_demo_menu.go, which imports the frozen
seed models - correct for that file, wrong for anything ordered after the
soft-delete conversion. The frozen ModelTime carried no comment at all, so
opening it taught the reader nothing.

Documents the boundary in three places the author actually passes through:
the frozen type itself, the contributor guide, and the module-scaffolding
skill, which previously said to copy the reference file verbatim and now
says to copy its structure but not its imports.

Claude-Session: https://claude.ai/code/session_01DJhM6LvhkNPej35wy9F7Aq
2026-08-31 15:01:26 +08:00
zhangwenjian 9bd542bb59 test: guard post-conversion migrations against the frozen seed models
Migrations ordered after 1786700003000 must not seed through
cmd/migrate/migration/models. That package's ModelTime declares a nullable
gorm.DeletedAt, which is the shape the columns had until that migration
converted deleted_at to a NOT NULL millisecond marker.

Afterwards it breaks in both directions. Writes put NULL into a NOT NULL
column and fail on the first insert. Reads are scoped "WHERE deleted_at IS
NULL" while live rows hold 0, so they match nothing - and 1786700001000
looks the admin role up that way and treats ErrRecordNotFound as "roles are
not seeded yet, skip authorisation", which would leave a module seeded with
no permissions and the migration still recorded as applied.

A fresh database does not surface either one: every migration using that
package today is ordered before the conversion, so it runs while the column
is still nullable. Only a migration added afterwards hits it.

Also pulls the import scan out of importsRuntimeModels so both checks share
one implementation, and derives the version through migration.GetFilename
rather than a second filename-parsing rule.

Verified by adding a violating migration and confirming the test fails with
an actionable message, then removing it and confirming the suite passes.

Claude-Session: https://claude.ai/code/session_01DJhM6LvhkNPej35wy9F7Aq
2026-08-31 15:01:11 +08:00
zhangwenjian 0fa015b6d0 perf👌: stop recompiling patterns when scanning the casbin exclusion list
AuthCheckRole walks CasbinExclude for every non-admin request, and used
casbin's util.KeyMatch2 to test each entry. That delegates to
util.RegexMatch, which is regexp.MatchString - it compiles its pattern on
every call - so a 32-entry list cost about 2,566 allocations per request
before the request reached Enforce.

Test the method first, which rules out most entries with a string
compare, and take the path test from go-admin-core, whose KeyMatch2
answers the same thing without recompiling. The scan drops to 52ns and no
allocations.

The loop moves out of AuthCheckRole so the tests exercise the code a
request runs rather than a copy of it, and an allocation budget fails if
the uncached matcher comes back.
2026-08-31 14:01:32 +08:00
zhangwenjian ec7d838ebd fix🐛: key the casbin enforcer by tenant host
setupSimpleDatabase runs once per configured database - one per host in
the multi-tenant configuration - and passed the same empty key to
mycasbin.Setup every time. Setup caches per key, so every host after the
first was handed the enforcer built from the first host's database and
was authorized against a casbin_rule table that was not its own.

Takes effect with the go-admin-core release that keys the cache; before
it, Setup ignored the argument entirely.
2026-08-30 10:03:26 +08:00
wenjianzhang 26e116c16c Merge pull request #884 from go-admin-team/ci/skip-deploy-for-docs
ci🔧: skip the deploy workflow for documentation-only changes
2026-08-29 14:49:39 +08:00
zhangwenjian 90d98893f5 ci🔧: skip the deploy workflow for documentation-only changes
A push to master here does not just build: it pushes an image, runs the
migrations and restarts the demo container, so the site takes a short outage
each time. The last two merges were markdown only and both paid for it.

Beyond the waste, a deploy can fail for reasons unrelated to the change that
triggered it - a container that will not come up, a database that is briefly
unreachable - and a README edit should not be able to turn the demo red.

Only build.yml is filtered. go.yml still builds on every push and pull request,
so nothing loses its compile check, and the badge keeps reporting the same
workflow it reports today.
2026-08-29 14:29:05 +08:00
wenjianzhang 10f162bf5d Merge pull request #883 from go-admin-team/fix/readme-vitepress-syntax
docs📝: drop VitePress container syntax GitHub cannot render
2026-08-29 14:28:11 +08:00
zhangwenjian 19909746f5 docs📝: drop VitePress container syntax GitHub cannot render
`:::tip` and its closing `:::` are VitePress custom containers. GitHub has no
such syntax, so both markers rendered as literal text: a paragraph beginning
":::tip" and a stray ":::" sitting alone above the next heading.

The Chinese README carries the same warning as a plain paragraph, which GitHub
renders correctly, so the English one now matches it. Verified through GitHub's
own markdown API: the literal marker no longer appears in the output and the
warning survives as ordinary text.

Only README.md was affected; the other three never had it.
2026-08-28 20:36:24 +08:00
wenjianzhang 205febdb8a Merge pull request #882 from go-admin-team/docs/readme-links-and-languages
docs📝: fix the badges and links, add Traditional Chinese and Japanese READMEs
2026-08-28 20:32:13 +08:00
zhangwenjian 5aec4ba32b docs📝: add Traditional Chinese and Japanese READMEs
The project had English and Simplified Chinese. These two follow the same
structure - same sections, same code blocks, same contributor list - so a
reader in any of the four sees the same document.

The Traditional Chinese is a translation rather than a character conversion of
the Simplified: the terminology differs (設定檔, 資料庫, 選單, 程式碼產生,
排程任務, 相依套件), and a converted file would read as machine output to
anyone who actually uses it.

Language navigation across all four is unified in the same commit, since a link
to a file that does not exist yet would be worse than no link.
2026-08-28 20:19:50 +08:00
zhangwenjian 8f1ea50dfe docs📝: point the badges and documentation links at the right places
The build badge rendered "build - failing" on both READMEs while CI was green.
It referenced the workflow under the old personal repository path, where the
status has been stale for years - so the first thing anyone saw on opening the
project was a failed build. It now points at the current repository, names the
workflow file explicitly, and pins the branch, so it reports master rather than
whatever happens to be the default branch later.

The workflow it reports on is go.yml, which is what the old badge referenced
by workflow name and is the right one to show: build.yml also deploys the demo
site, so a server-side problem there would turn the badge red while the code
is fine.

The licence badge read from mashape/apistatus, the example repository from
shields.io's own documentation. It happened to show MIT, the same licence this
project uses, so nobody noticed - but it reports someone else's licence.

Documentation links were spread across three hosts: doc.go-admin.dev redirects
to www.go-admin.pro, www.go-admin.dev serves byte-identical content, and only
the Chinese README linked the canonical host at all. All of them now point at
www.go-admin.pro directly rather than relying on a redirect outliving the
domain that issues it.

Two smaller ones: the gorm link pointed at the archived v1 repository while the
project builds on gorm.io v2, and the English introduction listed two UI kits
where the Chinese listed three, with an Ant Design demo linked directly below.
2026-08-28 20:19:50 +08:00
wenjianzhang 1483ca401d Merge pull request #881 from go-admin-team/fix/production-defaults
fix🐛: the settings a deployment needs, and the ones that were leaking
2026-08-28 19:50:18 +08:00
zhangwenjian ed74623a73 test: add an end-to-end load test harness
Skipped unless GOADMIN_BENCH_ADDR points at a running server, so `go test
./...` is unaffected.

Reports latency percentiles rather than an average, which is what capacity
planning needs, and a status-code distribution - that last part is how the rate
limiter's 200-on-rejection was found, since throughput alone looked excellent
while nothing reached a handler.

Includes a routing-floor control case. When a business endpoint matches it, the
measurement has stopped describing the endpoint and started describing the
transport, or the load generator when both share a machine.
2026-08-28 19:42:38 +08:00
zhangwenjian 1bc2e22833 fix🐛: give the config templates the defaults a deployment actually needs
Two settings that decide whether a deployment survives load, neither of which
appeared in any template.

The connection pool. Left unset, Go's defaults apply, and MaxIdleConns is 2:
under load almost every request opens a TCP connection and closes it again,
local ports run out, and the process answers "can't assign requested address"
to everything. Not slower - unavailable. A sweep against MySQL collapsed to
zero successful responses at 64 concurrent requests without these, and served
13,846 req/s with no errors once they were set.

The queue buffer. poolSize is the point at which messages start being dropped,
not a tuning knob: a full queue discards the message and returns an error
rather than blocking, and each stream has one consumer goroutine writing to the
database. At the previous default of 100 a load test lost over 60% of them; at
1000, none. Login and operation logs travel this queue, so what gets lost is
audit data - though only when logger.enableddb is on.

Both carry the reasoning in the file, because the failure mode of each is
invisible until it happens in production.
2026-08-28 19:42:38 +08:00
zhangwenjian cd8edfa5d4 fix🐛: reject rate-limited requests with 429 and make the threshold configurable
A rejected request answered 200 with the failure only in the body, so every
layer that reads the status line counted it as served: load balancers, metrics,
client-side retry. A load test against this reported the limiter's own
rejections as successful traffic and overstated throughput more than tenfold.

The threshold was a constant in the middleware, which made 200 QPS the ceiling
of every deployment with nothing in the configuration to reveal it. It now
reads extend.rateLimit.inboundQPS; an absent value keeps 200, so an upgrade
changes nothing, and zero disables the limiter for a deployment behind its own
gateway.

Also drops Strategy: system.BBR. Reading sentinel's source, the adaptive
strategy is consulted only for Load and CpuUsage - for InboundQPS the trigger
count is compared directly - so it read as if the limit adapted to the machine
when it never did.
2026-08-28 19:42:21 +08:00
zhangwenjian dcc2c8e175 fix🔒: stop logging the captcha answer
The answer was written at info level on every captcha request, so a currently
valid answer sat in the application log. Anyone able to read the log - an
operator, a log aggregator, anything that ships logs off the host - could
bypass the check the captcha exists to enforce.

The default log level records it, so this was not limited to debug builds.
2026-08-28 19:42:21 +08:00
zhangwenjian d991a285ba chore🔧: upgrade go-admin-core to v2.2.0
Carries four concurrency fixes and a bounded in-memory cache. The two that
reach this repository are the search resolver, which no longer panics on an
unexported field in a DTO and skips tag parsing for zero-valued ones, and the
captcha driver, which is built once rather than per request.

The cache bound does not apply here: config.CacheConfig.Setup() returns the
older Memory implementation, which core leaves unbounded.
2026-08-28 19:42:19 +08:00
wenjianzhang 76c9d1211e Merge pull request #880 from go-admin-team/fix/dsn-in-logs
ci🔧: migrate before deploying, roll back on failure — and stop logging the database password
2026-08-27 17:31:52 +08:00
zhangwenjian f5273f5a58 ci🔧: migrate before deploying, and roll back when the new version does not come up
Closes #871.

The deploy did docker rm -f then docker run. Nothing ran migrations, so
new code met old tables, and nothing checked the result - a container
that exits immediately left the site down with a green deploy.

Now, in order: pull the image, run the migration with it, and only then
touch what is running. A failed migration stops there, leaving old code
with the old schema, which is at least self-consistent.

The running container is renamed rather than removed, so it can be
started again unchanged if the new one does not become healthy. Healthy
means both an HTTP response and a database connection in the log: the
captcha endpoint answers without touching the database, so it alone
would call a container healthy that cannot reach MySQL.
2026-08-27 16:19:03 +08:00
zhangwenjian 54ffaac9c5 chore🔧: say which migration is running, not a column of ones
An applied migration printed its count - a bare '1' - so a database with
seven of them wrote seven lines of '1' at every start, and a failure said
only which error, never which migration.

It now names each one as it applies, reports the total, and says so when
there is nothing to do.
2026-08-27 16:19:03 +08:00
zhangwenjian 484de2e698 fix🔒: stop writing the database password into the log
The startup line printed the DSN whole:

  * => goadmin:<password>@tcp(host:3306)/go-admin?...

So every deployment wrote its own database credential into its own logs,
where a log shipper, a support bundle or a screenshot of a terminal
carries it onward. Found while reading deploy output, which is exactly
how it leaks.

The host and username stay - they are what makes the line worth printing
- and only the password is replaced. Both DSN shapes this project accepts
are covered, a sqlite path is left alone, and anything unparseable is
withheld rather than echoed, since it may hold a credential too.
2026-08-27 16:17:12 +08:00
wenjianzhang d72ff76aad Merge pull request #879 from go-admin-team/ci/config-path-secret
ci🔧: keep the host config path out of a public repository
2026-08-27 15:41:54 +08:00
zhangwenjian 4a523bed92 ci🔧: keep the host config path out of a public repository
The path is not a credential, and the file it points at is 600 and owned
by root, so this is not what protects it. But the repository is public
and there is no reason to publish the server's directory layout next to
the deploy that uses it.

DEMO_CONFIG_PATH holds it instead. It has to be set before this merges,
or the deploy stops at the guard - which is the intended failure: better
that than falling back to the sqlite in the image.
2026-08-27 15:37:09 +08:00
wenjianzhang 55dc33b865 Merge pull request #878 from go-admin-team/ci/demo-on-mysql
ci🔧: run the demo on the managed database instead of a bundled sqlite file
2026-08-27 15:33:15 +08:00
zhangwenjian 3d13f5856a ci🔧: point the demo at the managed database
The demo ran on the sqlite file baked into the image, so every deploy
reset it and nothing there resembled how anyone actually runs this.

The config is mounted from the host rather than taken from the image.
config/settings.demo.yml ships in a public repository and is copied into
a public image, so the connection string cannot live there; that copy
stays on sqlite, which is what a fresh clone should get.

The deploy refuses to start if the host config is missing, rather than
falling back to the image's sqlite and looking like it worked.
2026-08-27 12:48:32 +08:00
wenjianzhang aa2976ba17 Merge pull request #877 from go-admin-team/fix/mysql-fresh-install
fix🐛: MySQL installs could not log in — the migration run stopped at a tinyint overflow
2026-08-27 12:18:04 +08:00
zhangwenjian 8e141ff8a0 fix🐛: the code generator listed no tables at all
sys_columns and sys_tables were left out of the soft-delete conversion in
1786700003000. Their runtime models embed common.ModelTime, which is the
millisecond marker, so GORM queries them with deleted_at = 0 - against a
nullable datetime column holding NULL. Every row was invisible.

The repository carries two ModelTime types: the one under
cmd/migrate/migration/models still has a nullable gorm.DeletedAt and is
what builds the tables, while common/models has the marker and is what
queries them. Nothing connected the two, so a table could be built one
way and read the other with no signal at all.

The test now walks app/ for models embedding the marker and requires a
migration to cover each. tb_demo is exempt and says why: nothing reads it
at runtime.
2026-08-27 12:12:13 +08:00
zhangwenjian 2628ab8e3e fix🐛: a seeded menu overflowed its column and stopped the migration run
sort is gorm:"size:4", which MySQL builds as a tinyint holding -128..127.
The demo menu seeded Sort: 900, so on MySQL the run stopped at
1786700001000 with Error 1264, and every migration after it - including
the soft-delete conversion - never ran.

deleted_at therefore stayed NULL while the code queries deleted_at = 0,
and the login returned 'incorrect Username or Password' on a database
whose password hash was correct all along.

sqlite ignores the declared width, so a fresh install there passed and
the fault only appeared on MySQL.
2026-08-27 12:12:00 +08:00
wenjianzhang 1b7dcd843c Merge pull request #876 from go-admin-team/perf/data-permission
perf👌: the data-permission lookup ran on every request, including when it was switched off
2026-08-24 16:11:47 +08:00
zhangwenjian f0d91fb763 perf👌: read the data scope from the token instead of joining for it
The scope is decided by the user id, the role id, the department and the
data_scope string. Three of the four were already in the token; deptid
was not, though core's user.GetDeptId has always read that claim. Adding
it removes a sys_user join from every list, detail, update and delete.

This goes no more stale than rolekey does, which Casbin has read from
the token since the beginning: both settle on the next login.

A token minted before this still works. Its claims are incomplete, and
the lookup runs for it as before.
2026-08-24 15:36:11 +08:00
zhangwenjian 7238c6a26d perf👌: stop looking up a data scope that is switched off
Permission() returns the query untouched when EnableDP is false, so the
lookup feeding it has nothing to feed. The lookup ran anyway: a sys_user
join against sys_role on every list, detail, update and delete, with the
result discarded.

enabledp is false in settings.full.yml, so this was the default.
2026-08-24 15:35:33 +08:00
wenjianzhang 0964cf98e2 Merge pull request #875 from go-admin-team/fix/file-store-nil-client
fix🐛: the upload endpoint panicked on source=2, and cloud storage was never wired up
2026-08-24 15:00:31 +08:00
zhangwenjian 04c6a081ae fix🐛: source=3 uploaded to aliyun, and neither provider was ever configured
thirdUpload dispatched on the source parameter and then built the same
zero-value ALiYunOSS in both branches, so source=3 could not have
reached qiniu even with credentials.

Neither branch had credentials to use. OXS.Setup is the initialisation
path and nothing in the repository called it, and no configuration field
existed to fill. The store is now taken from extend.fileStore, and a
provider that was not configured says so rather than producing the
provider's own complaint about an empty bucket name.

The two handlers passed errors.New("") to e.Error, discarding what
actually went wrong; they now pass the error.
2026-08-24 13:25:54 +08:00
zhangwenjian fcbd9ae02e fix🐛: an unconfigured object store reports it instead of panicking
Each implementation keeps its provider client in an interface{} field that
Setup assigns, so an unconfigured store holds nil - and asserting nil to
the provider's client type panics:

  panic: interface conversion: interface {} is nil, not *oss.Client

The upload endpoint reaches that path for any request naming a provider
the deployment never configured.

Three more things were wrong in the same files. OXS.Setup printed a
failure and returned the store anyway, handing back exactly the broken
object that panics. HuaWeiOBS.UpLoad printed the provider's error and
returned nil, so a failed upload reported success. Both it and
QiNiuKODO.UpLoad asserted the local path was a string without checking.

The tests asked the reader to paste their own credentials, so they failed
for everyone who did not. They now cover the guards and skip the part
that needs a provider unless credentials are in the environment.
2026-08-24 13:23:07 +08:00
wenjianzhang d34d30a197 Merge pull request #874 from go-admin-team/ci/serialize-deploys
ci🔧: run one deploy at a time
2026-08-23 14:15:21 +08:00
zhangwenjian ecfea845c2 ci🔧: run one deploy at a time
Two merges seconds apart raced. Both runs do docker rm -f then docker
run; the second removed the container the first had just created, and
the first's docker run failed on the name conflict:

  Conflict. The container name "/go-admin-api" is already in use

The deploy went red and the demo stayed on the older image, which is the
worse half: a failure that leaves the wrong version running.

Grouping by ref serialises pushes to master while leaving pull request
runs independent, since those carry their own ref.
2026-08-23 14:11:49 +08:00
wenjianzhang e05ff7c809 Merge pull request #873 from go-admin-team/chore/drop-dockerfilebak
chore🔧: delete Dockerfilebak
2026-08-23 14:03:34 +08:00
wenjianzhang 28a9626661 Merge pull request #872 from go-admin-team/chore/skill-new-business-module
docs📝: add the new-business-module skill, and keep the rest of .claude out
2026-08-23 14:03:28 +08:00
zhangwenjian 96b2cb3acf chore🔧: delete Dockerfilebak
Added in 2022 and never touched since. Nothing references it - not the
workflows, not the Makefile, not a script - and it could not build
anyway: it copies config/settings.yml out of the builder, and that file
is gitignored.

It is a leftover from when the image was built inside the container,
before that was replaced by copying a binary built on the runner. Its
MAINTAINER line was the last one in the repository; Docker deprecated
the instruction in favour of LABEL maintainer years ago.
2026-08-23 13:43:40 +08:00
wenjianzhang 87fe6b7d9b Merge pull request #864 from go-admin-team/chore/core-v2
chore🔧: move to go-admin-core v2
2026-08-23 13:43:19 +08:00
zhangwenjian 722de8ea65 chore🔧: move the generator templates to v2 as well
The code generator writes Go files, and its templates still spelled the
old import paths, so a module generated after this migration did not
compile: the router it emits declares InitBusinessRouter with the v1
*GinJWTMiddleware while common.AuthInit now returns the v2 type.

Two of the paths moved rather than gaining a /v2 segment - the jwtauth
and response shims under sdk/pkg are gone in v2 - so this is not the
same rewrite the Go files got.
2026-08-23 13:28:17 +08:00
zhangwenjian 8ffde94433 chore🔧: move to go-admin-core v2
Every import of the module changes, not only the seven packages that
moved out of sdk/pkg: Go requires the major version in the path from v2
on. Both happen in one pass —

    go run github.com/go-admin-team/go-admin-core/tools/coreupgrade@v2.0.0 -w -v2 .
    go mod tidy

— which is the command the release notes give, run here as a consumer
would run it. 210 imports across 95 files.

The compatibility shims this used are gone in v2, so the paths that
moved had to move: sdk/pkg/captcha, sdk/pkg/jwtauth and its user
package, sdk/pkg/response and sdk/pkg/casbin.

The count of unformatted files is unchanged at 34, none of them touched
by this: the tool reformats a file only if it was already gofmt clean,
so a migration cannot disappear into whitespace.
2026-08-23 13:26:46 +08:00
wenjianzhang 08eef12bca Merge pull request #865 from go-admin-team/fix/codegen-inverted-guards
fix🐛: the code generator's guards were all written backwards
2026-08-23 13:24:50 +08:00
zhangwenjian ab0e8e6056 docs📝: add the new-business-module skill, and keep the rest of .claude out
The skill walks a single-table CRUD module end to end: migration, the
Actions-mode model, dto and router, and the sys_menu / sys_api /
casbin_rule seed data without which the module builds but never appears.

.claude was ignored wholesale. Un-ignoring the skills directory would
have committed every skill put there, including personal ones, so the
skills that ship are re-included one directory at a time.

AGENTS.md now points at 1786700001000_demo_menu.go for the seed data,
which is the runnable version of what the skill describes.
2026-08-23 13:20:26 +08:00
zhangwenjian 2cef52d906 chore🔧: record the modules the code imports as direct
go mod tidy moves glebarez/sqlite and gorm.io/plugin/soft_delete out of
the indirect block: the tests import the first and common/models the
second. CI runs tidy before building, so the tree was dirty from the
first command.
2026-08-23 13:19:15 +08:00
zhangwenjian 95077b116b fix🐛: the candidate table query assumed one database
The exclusion list was a subquery against `$GenConfig.DBName`.sys_tables,
so it named the schema by hand. Generating from a schema that is not the
one holding sys_tables made the whole query fail, and because the
subquery read the table directly it also counted soft-deleted entries:
deleting a generator entry never handed its table back.

Read the registrations through the model on this connection instead. An
empty list skips the clause - NOT IN (NULL) is unknown for every row,
which would leave a fresh install with nothing to generate from.
2026-08-23 13:19:15 +08:00
zhangwenjian d4cf11d313 fix🐛: report an unknown database driver instead of panicking
opens is a map, so opens[c.Driver] on a driver this build does not carry
returns a nil function, and gorm.Open calls it. The operator saw a nil
dereference inside gorm with nothing naming the driver.

sqlite3 is the case that bites: it needs cgo and is only compiled in
under the sqlite3 build tag, so the same config file works on one binary
and dies on another. Resolve the driver first and say which ones this
build supports.
2026-08-23 13:19:15 +08:00
zhangwenjian f201792d8f fix🐛: the mysql-only guard never fired
pkg.Assert panics when its condition is false, so pkg.Assert(true,
"目前只支持mysql数据库") is a no-op. On postgres or sqlserver the code
generator did not report that it needs MySQL: DBTables returned an empty
list with a nil error, and DBColumns ran its query on the zero-value
*gorm.DB left over from the branch that never assigned, which is a nil
dereference rather than a message.

Assert the driver up front instead of asserting a constant in an else,
which also removes the placeholder *gorm.DB the fall-through relied on.
DBColumns.GetPage had no guard at all and gets the same one.
2026-08-23 13:19:15 +08:00
zhangwenjian d16f5e7180 fix🐛: db columns endpoint rejected exactly the valid requests
pkg.Assert panics when its condition is false, so
Assert(TableName == "", "table name cannot be empty") rejected every
request that carried a table name and let the empty one through. The
model layer repeated the inversion with if TableName != "" { return
error }, so either one alone was enough to break the endpoint.

Flip both, and hoist the model guard out of the mysql branch so it
matches GetList ten lines below, which had it right all along.
2026-08-23 13:19:15 +08:00
wenjianzhang f8f697af2b Merge pull request #868 from go-admin-team/ci/stop-gitee-mirror
ci🔧: stop mirroring the repository
2026-08-23 13:18:19 +08:00
wenjianzhang eb8da38a46 Merge pull request #870 from go-admin-team/fix/demo-db-soft-delete
fix🐛: the soft-delete migration could not run, and the demo database never got it
2026-08-23 13:01:17 +08:00
zhangwenjian f19568c69a chore🔧: bring the bundled demo database up to the current migrations
go-admin-db.db ships in the repository and the Dockerfile copies it into
the image, which then runs only the server. Its last recorded migration
was from 2022, so every row still carried a null deleted_at while the
code queries deleted_at = 0. Nothing matched: not the login, not the
sixty-seven menus, not the five departments.

Anyone starting from the bundled sqlite database met the same wall, and
the failure reads as an incorrect username or password.
2026-08-23 12:57:05 +08:00
zhangwenjian 91bf25e5fe fix🐛: the soft-delete migration could not run against the real schema
Two assumptions held on the test's table and on nothing else.

It dropped deleted_at while an index still referred to it. MySQL and
PostgreSQL drop dependent indexes along with the column; SQLite refuses,
and the migration stopped at the first table with such an index - which
is all thirteen of them.

It also read the rows through a column named id. sys_dept keys on
dept_id, sys_user on user_id, and only some tables on id, so the pass
that carries the deletion timestamps across never ran.

The test's table had an id key and no index on deleted_at, which is
exactly the shape that lets both through. It now matches sys_user.
2026-08-23 12:56:50 +08:00
wenjianzhang 85666f160f Merge pull request #867 from go-admin-team/docs/readme-refresh
docs📝: repoint the README links that stopped resolving
2026-08-22 23:12:49 +08:00
zhangwenjian b2baf48dc6 ci🔧: stop mirroring the repository
Every push mirrored to Gitee and GitLab. Neither mirror is wanted any
more, so the workflow goes rather than half of it.

The GITEE_KEY and GITLAB_KEY secrets are left in place; restoring the
mirror is a revert of this commit.
2026-08-22 17:34:50 +08:00
zhangwenjian c906e1d503 docs📝: repoint the links that stopped resolving
The two tutorial links pointed at doc.zhangwj.com, which no longer
answers; the same paths serve from doc.go-admin.dev. golangroadmap.com
returns 503. The jwt-go credit pointed at dgrijalva/jwt-go, archived
years ago - this project builds on golang-jwt/jwt.

Also: the copyright years said 2022 and 2024, the English README asked
for a password in Chinese, and the Chinese README's link section lost
its only entry, so it gets the one the English side already had.
2026-08-22 15:26:44 +08:00
wenjianzhang 3b93ac19f8 Merge pull request #863 from go-admin-team/fix/soft-delete-groundwork
fix🐛: a unique constraint the database can actually keep
2026-08-22 11:59:12 +08:00
zhangwenjian 9914373d45 test🧪: run the assertion through the function it is about
Review caught that this reissued getByRoleName's query instead of
calling it, so it passed whether or not the production line still said
what it was supposed to — a test named for a change it did not touch.

It calls getByRoleName now, and restoring the hand-written clause fails
it for exactly the reason this PR exists: with the marker non-null,
"deleted_at is null" matches nothing and the query returns an empty
list.
2026-08-22 11:40:38 +08:00
zhangwenjian 4911730012 fix🐛: give the natural keys a constraint the database can keep
sys_user.username, sys_role.role_key and sys_dict_type.dict_type had no
unique index. Uniqueness was a SELECT COUNT followed by an INSERT, which
two concurrent requests both pass — and login resolves a username with
First, so which of the two accounts answers is whichever the database
returns.

The index cannot be on the key alone, because a soft-deleted row keeps
occupying the name and a deleted user's username could never be used
again. It has to include the delete marker, and the marker has to be
non-null: two live rows are (alice, NULL) and (alice, NULL), and NULL is
not equal to NULL, so an index over a nullable marker admits both. That
is the worst of the three states — a constraint that reads as protection
and binds nothing — and there is a test that demonstrates it rather than
asserting it.

ModelTime.DeletedAt is milliseconds since the epoch now, zero while the
row is live. Sixteen tables carry it; the migration converts each one,
preserving when each deleted row was deleted, then adds the three
indexes.

Written to be re-runnable rather than transactional, because DDL does not
roll back on MySQL and an operator whose first attempt failed halfway
should have nothing to do but run it again. It refuses before altering
anything if a table already holds duplicates, naming them, rather than
letting the index fail and leaving the operator to guess.

The timestamp conversion happens in Go: turning a timestamp into epoch
milliseconds is spelled differently by every dialect this supports, and
these row counts do not justify four versions of it.
2026-08-22 11:27:22 +08:00
zhangwenjian 88bab51056 fix🐛: stop hand-writing the soft-delete condition, and check the count
Two things in front of the unique-index work, both safe on their own.

getSysMenuByRoleName carried "deleted_at is null" in its where clause.
GORM adds that condition itself for a model with a DeletedAt field, so
it was a duplicate — and one phrased as a column being null, which stops
being true the moment the column stops being nullable. A schema that
moves to a non-null delete marker would have turned this query into one
that matches nothing, silently, for admin users only.

SysDictType.Insert dropped the error from its duplicate check: a query
that failed left the count at zero and the insert went ahead as though
the name were free.

The test pins what the removed clause was there for. Its counter-proof
is Unscoped rather than deleting the field — taking ModelTime off the
model fails to compile, which proves nothing.
2026-08-22 11:11:03 +08:00
wenjianzhang 0fd4f68b6c Merge pull request #861 from go-admin-team/docs/fix-stale-queue-redis-sample
fix: correct the commented-out queue.redis sample in settings.yml
2026-08-20 11:01:03 +08:00
zhangwenjian c66cb5c6a8 fix: correct the commented-out queue.redis sample in settings.yml
The sample had producer/consumer nested keys (streamMaxLength,
approximateMaxLength, visibilityTimeout, bufferSize, concurrency,
blockingTimeout, reclaimInterval) that don't exist on config.RedisQueue —
checked against sdk/config/queue.go, which only reads addr, password, and the
embedded RedisOptions fields, plus group, key_prefix and max_attempts. Filling
in the old sample as written would compile and start fine, since it's YAML
under a key the struct doesn't declare, and every one of those settings would
be silently ignored.

Replaced with the fields the struct actually has. Still commented out —
redis stays opt-in, this only fixes what filling it in would produce.
2026-08-20 10:54:53 +08:00
wenjianzhang b16ec0af77 Merge pull request #860 from go-admin-team/chore/upgrade-core
chore🔧: upgrade go-admin-core and route the queue through configuration
2026-08-18 22:48:00 +08:00
zhangwenjian 82ea8539eb chore🔧: upgrade go-admin-core and route the queue through configuration
The pinned core dated from April, before sdk stopped being a separate module,
so the build resolved sdk packages from the old module and core packages from
the new one. Dropping the separate requirement is what makes the two agree
again.

Most of the diff is renames that came with that: the tenant accessors gained a
ByTenant suffix, GetDb now returns one database and GetAllDb the map, and
casbin moved to v3.

The change that matters is four call sites moving from GetMemoryQueue to
GetQueuePrefix. GetMemoryQueue returns a queue fixed at construction, so the
login log, the operate log and the api check ran in process no matter what the
settings file selected — a second instance saw none of it. GetQueuePrefix
returns whatever the configuration built, which is the point of being able to
configure a queue at all.

Verified against core at main: build and vet clean. The two file_store failures
are unchanged from before this branch; they need cloud credentials.
2026-08-18 22:14:08 +08:00
wenjianzhang d17d5c1206 Merge pull request #859 from go-admin-team/docs/clarify-demo-sites
docs📝: 标注 antd 演示站对应 go-admin-pro
2026-08-16 19:31:25 +08:00
zhangwenjian 041d22d0d2 docs📝: 标注 antd 演示站对应 go-admin-pro
README 中两个演示地址并排列出、格式与账号密码完全相同,看不出 antd 站对应
的是另一个产品。用户在该站遇到问题时会认为是本仓库的缺陷(见 #857:登录
返回的错误码在本仓库中并不存在)。

仅在链接文字中补充产品名,不改变呈现方式。
2026-08-16 12:04:26 +08:00
wenjianzhang 5864058a81 Merge pull request #858 from go-admin-team/chore/bump-version-2.4.0
chore🔧: 版本号升至 2.4.0
2026-08-16 11:17:51 +08:00
zhangwenjian 45035a16e4 chore🔧: 版本号升至 2.4.0 2026-08-16 11:14:28 +08:00
wenjianzhang f4d0108d49 Merge pull request #855 from go-admin-team/fix/remove-refresh-token-endpoint
fix🐛: 移除 refresh_token 接口,修复 token 可无限续期问题
2026-08-16 11:08:41 +08:00
zhangwenjian b81611ba72 chore🔧: 清理 refresh_token 的残留权限数据
接口移除后,库中仍留有三类记录:sys_api 的接口登记、sys_menu_api_rule 的
菜单绑定、casbin_rule 的策略。留着会让「接口管理」列出一个不存在的端点,
角色配置里也仍可勾选。

- 新装:从 db.sql 与 db-sqlserver.sql 的种子数据中删除该接口
- 已有部署:新增迁移清理,按 path 匹配而非固定 id,因为执行过
  `server -a` 重新注册接口的库中 id 会与官方种子数据不同
2026-08-14 21:42:59 +08:00
zhangwenjian bb34108831 fix🐛: 移除 refresh_token 接口,修复 token 可无限续期问题
close #820

GET /api/v1/refresh_token 用业务 token 即可换取新 token,而续期上限
MaxRefresh 依据的 orig_iat 在每次续期时被一并重置,上限永远无法到达 ——
token 一旦泄露即等同于永久访问权,且无任何吊销手段。

该路由此前还位于 CasbinExclude 中,不受 Casbin 约束,任何角色的已登录用户
都可调用。

官方前端从未使用它:store 中虽有 refreshToken action,但全仓库无一处
dispatch,属死代码。移除不影响正常登录与鉴权流程。

破坏性变更:自行调用该端点实现续期的使用者需改为重新登录。正确的无感续期
应在 go-admin-core 中区分 access token 与 refresh token 后重新实现,不应
沿用此路由。
2026-08-14 21:42:52 +08:00
wenjianzhang b7fd92f39b Merge pull request #854 from go-admin-team/docs/agents-and-demo-module
feat: 新增 app/demo 参照模块与 AGENTS.md 规范文档
2026-08-14 21:37:57 +08:00
zhangwenjian 63b800a3ba docs📝: 补充 sqlite3 构建标签与迁移目录说明
driver 配置为 sqlite3 时不带 -tags sqlite3 会在 nil 函数上 panic,
报错不提及构建标签,容易误判为环境损坏;同时说明 version/ 与
version-local/ 的区别,后者已被 gitignore,提交到本仓库的迁移必须放 version/。
2026-08-14 21:17:37 +08:00
zhangwenjian ed9450a2d5 feat: 补充 demo 模块的菜单与权限种子数据
一个业务模块要在界面上可用,需要四类数据协同:

  sys_api           后端路由登记,Casbin 据此判定
  sys_menu          侧边栏菜单,含目录 M、菜单 C、按钮 F 三级
  sys_menu_api_rule 菜单与接口的关联,角色保存时据此生成策略
  casbin_rule       实际生效的权限策略

菜单的 menu_name 与前端组件 name 保持一致(DemoProduct),按钮的
permission 与前端 v-permisaction 标识一致(demo:product:add 等)。

策略写入 casbin_rule 而非 sys_casbin_rule:后者对应的 models.CasbinRule
是历史遗留,其 7 列 size:512 唯一索引在 MySQL 下会超出索引长度限制,实际
生效的是 adapter 创建的 casbin_rule 表。

所有写入均为存在则更新、不存在则插入,迁移可安全地在已有数据的库上执行。
实测:在含 67 条菜单、121 条接口的库上执行后各表数据正确;清除版本记录重
跑一次,各表行数不变,确认幂等。
2026-08-14 16:57:55 +08:00
zhangwenjian 1d551a10ab docs📝: 新增 AGENTS.md 与架构说明
AGENTS.md 是给 AI 编码工具与新贡献者的约定,只记录「不遵守就会出错」的
规则,技术栈版本与命令交由 go.mod 和 Makefile 表达,避免文档与代码脱节。
标准写法指向 app/demo/——那是可编译、有测试的参照物,文档与它冲突时以它
为准。

docs/architecture.md 承载不易从代码直接读出的语义:DataScope 五档的过滤
方式、定时任务的 JobExec 接口、多数据源约束、迁移目录的分工。

内容整理自此前未纳入版本控制的 CLAUDE.md,撰写时逐条对照代码核实,修正
了其中两处失效描述(构建工具已非 Vue CLI;JobExec 的方法是 Exec(interface{})
而非 Run(string))。CLAUDE.md 现改为指向 AGENTS.md 的软链,两者不再各自
漂移。
2026-08-14 16:49:48 +08:00
zhangwenjian 4d7c9e5a12 feat: 补充 demo 模块的建表迁移
放在 version/ 而非 version-local/:后者已被 .gitignore 忽略,是留给使用
者存放自身迁移脚本的位置,示例迁移需随框架一起分发。文件注释中说明了这
一区分。
2026-08-14 16:46:53 +08:00
zhangwenjian d1f5fe5681 feat: 新增 app/demo 标准 CRUD 参照模块
作为编码约定的可执行参照物:文档会滞后,而这个模块过时会导致构建或测试
失败,因此以它为准。

目录骨架与自动注册文件由项目自带的脚手架生成:

  go run main.go app -n demo

它同时产出 cmd/api/demo.go,其中的 init() 将路由追加进 AppRouters,
无需在任何中心文件手工登记。

模块本身演示了单表 CRUD 的推荐写法——直接使用 common/actions 提供的五个
通用 Action,因此只有 model、dto、router 三个业务文件,没有 apis 与
service。手写 Handler 的场景仅在业务超出单表 CRUD 时才需要。

DTO 中详情/删除入参内嵌 dto.ObjectById 以复用其 Bind 与 GetId,不重复
实现 uri 绑定与批量 ids 合并逻辑。

补充 8 项测试锁定通用 Action 的接口约束,其中最关键的是 Generate() 必须
返回副本——Action 在并发请求间复用实例,就地返回会串数据。反向验证:将
Generate 改为就地返回,测试立即失败。
2026-08-14 16:46:09 +08:00
zhangwenjian e8c2e0a966 chore🔧: 修正 .DS_Store 忽略规则
原规则 `*/.DS_Store` 只匹配子目录一层,仓库根目录下的 .DS_Store 不在其
中。改为 `.DS_Store`,匹配任意层级。
2026-08-14 16:45:55 +08:00
wenjianzhang cef0a19a9c Merge pull request #853 from go-admin-team/fix/community-pr-batch
fix🐛: 处理社区 PR 中仍然成立的四项修复
2026-08-14 15:46:43 +08:00
zhangwenjian c0e81363dc docs📝: 修正 Makefile 注释错别字
「实际决对路径」→「实际绝对路径」。

问题由 PR #847 指出。
2026-08-14 15:35:55 +08:00
zhangwenjian df2e4a2b48 fix🐛: 修正欢迎页 iframe 高度塌陷
页面通过 JS 计算并设置 iframe 高度,但 html 与 body 未声明高度,
百分比高度失去参照,iframe 在部分场景下塌陷为 0。

补充 html,body{height:100%} 与 iframe 的 height:100%,并为原先缺失的
overflow-y 声明补上分号。

问题由 PR #829 指出。
2026-08-14 15:35:55 +08:00
zhangwenjian 9088ebc2e1 refactor🎨: 修正文件名拼写 int_router.go → init_router.go
该文件内容为 init() 函数中的路由注册,原文件名少了一个字母。

问题由 PR #787 指出。
2026-08-14 15:35:55 +08:00
zhangwenjian 9f2dec3036 fix🐛: 修正 GeneralDelDto.GetIds 重复追加 Id
该方法先在开头追加了 Id,随后 else 分支中又追加一次:仅传 Id 时返回
[5 5],删除接口会对同一条记录执行两次 DELETE。

  if g.Id != 0 { ids = append(ids, g.Id) }
  if len(g.Ids) > 0 { ... } else {
      if g.Id > 0 { ids = append(ids, g.Id) }   // 重复
  }

去掉冗余分支,同时将首个判断由 != 0 收紧为 > 0,与 Ids 中逐个元素的
过滤条件保持一致(负数 Id 无意义)。

补充单元测试,覆盖仅 Id、仅 Ids、二者并存、含非正数、全空回退等场景。

问题由 PR #848 指出。
2026-08-14 15:35:55 +08:00
wenjianzhang ea049f9b06 Merge pull request #852 from go-admin-team/fix/ci-deploy-guard
fix🐛: 限制部署步骤仅在 master 收到 push 时执行
2026-08-12 15:41:26 +08:00
zhangwenjian 1f1349a685 docs📝: 更新在线体验地址
Element UI vue2 演示站已升级为 Element Plus + Vue 3,域名同步更换为
vue.go-admin.pro。

Arco Design vue3 演示站(vue3.go-admin.dev)已下线,移除对应条目。
2026-08-12 12:27:41 +08:00
zhangwenjian dcef2df38e fix🐛: 限制部署步骤仅在 master 收到 push 时执行
本工作流同时由 push 与 pull_request 触发,而推送镜像与重启服务两步没有
任何事件限制。其后果是:任何指向 master 的 PR 一经创建,就会把 PR 分支
构建出的镜像推送到镜像仓库,并 docker rm -f 掉线上容器、用该镜像重新启
动 API 服务——发生在代码被审查和合并之前。

同仓库分支发起的 PR 可以取到 secrets,因此该路径实际可达;历史运行记录
中已多次出现由 pull_request 事件触发的成功部署。

为两步加上 event_name 与 ref 双重判断。额外判断 ref 是考虑到日后若有人
向 on.push.branches 追加分支,部署不会随之扩散。

Tidy 与 Build 不受影响,PR 仍会执行编译校验。
2026-08-12 12:27:33 +08:00
zhangwenjian 92834d6e39 publish🚀: 版本号更新至 2.3.0 2026-08-12 00:22:31 +08:00
zhangwenjian f06540883b fix🐛: 修复 Docker 镜像发布的 tag 条件失效问题
if 表达式中不应使用 ${{ }} 包裹:startsWith(${{github.ref}}, 'refs/tags/')
会先将 github.ref 替换为裸字符串再参与表达式求值,导致条件判断失效,
使得每次 push 到 master 都会构建并推送镜像至 ghcr.io,而非仅在打 tag 时发布。

同时 on.push 缺少 tags 配置,打 tag 实际不会触发该工作流。

修正后:push 分支仅执行 Go 构建,打 tag 才发布镜像。
2026-08-11 11:09:39 +08:00
zhangwenjian 3c9ce5b6b0 chore🔧: 升级 x/image 修复 TIFF 解码漏洞 2026-08-10 22:17:28 +08:00
zhangwenjian ff8a59550a fix🐛: 修复镜像同步因浅克隆被拒绝的问题 2026-08-10 21:21:01 +08:00
zhangwenjian 7cddef33a2 git🙈: 将 go.sum 纳入版本控制 2026-08-10 20:51:00 +08:00
zhangwenjian 7013c2fa4a chore🔧: 移除依赖已封禁 action 的 issue 自动化流程 2026-08-10 20:51:00 +08:00
zhangwenjian 65bacacb38 docs📝: 更新 README 环境要求版本说明 2026-08-10 20:45:39 +08:00
zhangwenjian 45587028c3 config🔧: CI 升级 Go 版本并更新 Actions 至最新 2026-08-10 20:45:39 +08:00
zhangwenjian 887c9cca4b chore🔧: 升级 Go 至 1.26.5 并同步升级依赖 2026-08-10 20:45:36 +08:00
wenjianzhang a6ddb113fc Update LICENSE.md 2026-08-08 13:55:08 +08:00
wenjianzhang b83eef8670 Fix image source in README.md
Updated image source in README.md for go-admin.
2026-05-22 11:20:39 +08:00
zhangwenjian 43dcd61c51 config🔧: update go-version to 1.24 in build workflow
go.mod requires go 1.24, go mod tidy fails when runner uses 1.18.
2026-05-15 17:52:12 +08:00
zhangwenjian 1bd64d4562 config🔧: pin all GitHub Actions to full-length commit SHAs
Replace version tags (@v1/@v2/@v3/@master) with pinned commit SHAs
across all workflow files to satisfy go-admin-team organization
security policy requiring immutable action references.
2026-05-15 17:48:41 +08:00
zhangwenjian 44e81bc72f git🙈: 补充忽略本地开发配置文件
- 新增忽略 config/settings.local.dev.yml
2026-05-15 17:37:42 +08:00
zhangwenjian 3312f8b7b9 chore🔧: 升级依赖 mergo 模块路径
- 替换 github.com/imdario/mergo 为上游迁移后的 dario.cat/mergo v1.0.1
2026-05-15 17:37:42 +08:00
zhangwenjian d6a2272f9d git🙈: 完善 .gitignore 忽略规则
- 新增忽略编译产物 go-admin-server
- 新增忽略本地工具配置目录
2026-05-15 17:37:42 +08:00
wenjianzhang a5cc0a9e29 Add read and write timeout to HTTP server 2025-09-10 09:39:54 +08:00
wenjianzhang 3f995735e9 Merge pull request #834 from hosea3000/edit-no-confirm
点击编辑的时候不需要弹框确认,交互不太友好
2025-05-20 11:41:02 +08:00
wenjianzhang b65b74dee5 Merge pull request #832 from hosea3000/fix-number-input
fix🐛: 修复自动生成代码时选择字段类型为int64, 前端提交还是string 导致报错的问题
2025-05-20 11:40:21 +08:00
Hosea 98cf3ad95a fix🐛: 点击编辑的时候不需要弹框确认,交互不友好 2025-05-20 10:57:36 +08:00
Hosea 8649d8d791 fix🐛: 修复自动生成代码时选择字段类型为int64, 前端提交还是string 导致报错的问题 2025-05-13 15:48:45 +08:00
wenjianzhang 817e34c6aa refactor🎨: 重构文件上传逻辑,拆分处理函数以提高可读性和维护性 2025-04-13 22:20:06 +08:00
wenjianzhang 952cd92648 refactor🎨: 清理 sys_server_monitor.go 文件,移除未使用的导入并格式化代码 2025-04-13 22:17:17 +08:00
wenjianzhang 6b1e961a7f refactor🎨: 重构系统监控代码,拆分功能为多个函数以提高可读性和维护性 2025-04-13 22:15:10 +08:00
wenjianzhang 762eba5af7 refactor🎨: 重构 Setup 函数,拆分为多个子函数以提高可读性和维护性 2025-04-13 22:04:12 +08:00
wenjianzhang e82128f679 refactor🎨: 优化获取客户端 IP 的逻辑,增加对 X-Forwarded-For 和 X-Real-IP 的处理 2025-04-13 22:00:04 +08:00
wenjianzhang 8f8a197db1 delete🎉: 移除示例代码 run.go 2025-04-08 20:49:36 +08:00
wenjianzhang 364854eda0 docs📝: 更新 go-admin 版本号至 2.2.0 2025-04-08 20:49:29 +08:00
wenjianzhang b259e91f4d Merge remote-tracking branch 'origin/master'
# Conflicts:
#	go.mod
2025-04-08 20:45:25 +08:00
wenjianzhang 76411f80bc refactor🎨: 优化日志记录方式,统一使用 log.Info 替代 log.Println 2025-04-08 20:30:35 +08:00
wenjianzhang 5494353229 fix🐛: 修复获取本地主机IP的函数调用错误 2025-04-08 20:30:24 +08:00
wenjianzhang afe5efbe36 refactor🎨: remove unused distributed lock setup code in initialize.go 2025-04-08 20:30:05 +08:00
wenjianzhang db422785fc fix🐛: include captcha answer in GenerateCaptchaHandler for improved logging 2025-04-08 20:29:38 +08:00
wenjianzhang 4ac68323da fix: improve error logging in jobbase.go for better clarity 2025-04-08 20:23:23 +08:00
wenjianzhang 44002fcb11 chore: update dependencies in go.mod to latest versions 2025-04-08 20:22:59 +08:00
wenjianzhang 5bbd919745 chore: update dependencies in go.mod to latest versions 2025-03-25 17:16:40 +08:00
wenjianzhang 54dd3de5b6 chore: update dependencies in go.mod to latest versions 2025-03-25 16:48:47 +08:00
wenjianzhang 9540fdfc30 refactor: remove unused GetMenuIDS function and clean up code 2025-03-25 16:44:31 +08:00
wenjianzhang 937775e2a7 chore: update Go version from 1.21 to 1.24 in build configuration 2025-03-25 08:53:39 +08:00
wenjianzhang 84721265dd fix: simplify error handling in GenerateCaptchaHandler 2025-03-24 22:41:02 +08:00
wenjianzhang 3ab67dfa7d chore: update Go version from 1.21 to 1.24 2025-03-24 20:53:09 +08:00
wenjianzhang 6a1941a820 Merge pull request #814 from keemozhang/master
fix🐛: declaration of new local variable causes transactions to be ign…
2025-03-21 15:36:16 +08:00
wenjianzhang 3ae7c44585 Merge pull request #816 from Tiper-In-Github/patch-1
Fix:err is never used
2025-03-21 15:35:23 +08:00
wenjianzhang 9d809f6392 Merge pull request #821 from pigwantacat/master
fix:修复定时任务的日志打印
2024-12-18 00:11:54 +08:00
pigwantacat 4b477b3103 fix:修复定时任务的日志打印 2024-11-01 14:14:30 +08:00
wenjianzhang e7ae2fe019 更新 go_admin.go 2024-10-30 22:12:17 +08:00
wenjianzhang d5ba3d9770 更新 READMEN.md 2024-10-30 22:10:06 +08:00
Akiraka f3d744f6f5 修复获取getinfo时候,userName 事件结果为 nickName 问题 2024-10-24 09:30:15 +08:00
无别 0315631b53 Fix:err is never used
Fix the problem that err is overwritten and becomes invalid
2024-09-29 15:32:36 +08:00
wenjianzhang 48e7ce88ff perf👌: rollback base64Captcha 2024-09-09 15:46:05 +08:00
wenjianzhang 357db6b1c9 Merge remote-tracking branch 'origin/master' 2024-09-08 22:04:20 +08:00
wenjianzhang 83e0531f43 perf👌: format 2024-09-08 22:04:08 +08:00
wenjianzhang 898ba7d8eb Update README.Zh-cn.md 2024-09-06 23:11:48 +08:00
wenjianzhang 8751f34539 perf👌: correct attribute definition 2024-09-05 18:33:11 +08:00
wenjianzhang 4aa0068d2d perf👌: update SysDept Get First to FirstOrInit 2024-09-05 18:29:50 +08:00
keemozhang b954a2f092 fix🐛: declaration of new local variable causes transactions to be ignored 2024-09-05 15:46:20 +08:00
wenjianzhang 9227bd2be1 perf👌: format code 2024-09-04 20:25:02 +08:00
wenjianzhang e70a0b1314 perf👌: update SysConfig Get First to FirstOrInit 2024-09-04 20:22:49 +08:00
wenjianzhang 21c262a31e perf👌: update build file 2024-09-03 22:17:11 +08:00
wenjianzhang f30889bd19 perf👌: update SysApi Get Func First to FirstOrInit 2024-09-03 21:22:09 +08:00
wenjianzhang 23e519999e perf👌: update go mod 2024-08-30 16:00:29 +08:00
wenjianzhang bedf064ace Merge pull request #802 from zhanluxianshen/drop-base-model
replace basemodel by common.model
2024-08-29 16:26:22 +08:00
wenjianzhang 9a8e0cddde Merge pull request #803 from zhanluxianshen/clean-err-use-in-method
clean err define in methods.
2024-08-29 16:23:54 +08:00
wenjianzhang c0c16036d3 Merge pull request #811 from wangle201210/fix/logger
fix🐛: reset default logger fields
2024-08-29 16:17:18 +08:00
wanna dd905a2bed fix🐛: reset default logger fields 2024-08-23 16:49:21 +08:00
zhanluxianshen 2d76430f89 clean err define in methods.
Signed-off-by: zhanluxianshen <zhanluxianshen@163.com>
2024-07-10 15:03:33 +08:00
zhanluxianshen 5dde1d2a00 replace basemodel by common.model
Signed-off-by: zhanluxianshen <zhanluxianshen@163.com>
2024-07-10 11:26:52 +08:00
lwnmengjing 93f25c6cdf Add mss-boot-io link 2023-11-07 23:28:52 +08:00
wenjianzhang d366df372d feat: Log file size control and retention days control 2023-11-03 18:59:20 +08:00
wenjianzhang 7281d05efc fix🐛: Fixed system startup Network output problem 2023-11-03 17:42:01 +08:00
wenjianzhang e6d6a65267 Merge remote-tracking branch 'origin/master' 2023-11-03 17:36:21 +08:00
wenjianzhang 9ff094b6f5 fix🐛: Fixed data migration issue during multi-tenant configuration 2023-11-03 17:36:04 +08:00
wenjianzhang fc9c253a9f tag📌: Upgrade go1.21 2023-11-03 17:35:01 +08:00
wenjianzhang 9d735ed5aa docs📝: Update README.md 2023-11-02 17:42:40 +08:00
wenjianzhang d782b00117 tag📌: Change version 2023-11-02 17:10:52 +08:00
wenjianzhang 239159dd2a fix🐛: Fix the problem that el-popconfirm does not take effect 2023-11-02 17:08:58 +08:00
wenjianzhang 0c1e91c3b5 Add files via upload 2023-10-11 21:25:01 -05:00
wenjianzhang c09347b387 Merge pull request #768 from majiayu000/fix-pgerror
[BugFix] 修复一个``引发的bug
2023-10-11 21:18:42 -05:00
lif e49e47c7a1 Delete go.mod 2023-09-22 14:02:38 +08:00
wenjianzhang 98b46535aa Merge pull request #767 from zgxme/fix-gen-0909
[fix](gen) ignore default time type columns in table
2023-09-21 22:00:57 +08:00
lif 014a23aac3 [BugFix] Fix pgsql error with 2023-09-14 16:35:41 +08:00
zgxme f1dfba79e0 [fix](gen) ignore default time type columns in table 2023-09-09 22:59:45 +08:00
wenjianzhang a282e44b1d Merge pull request #753 from Vingurzhou/master
-installsuffix 参数没有指定后缀字符串。它被设定为空,这可能导致一些问题
2023-08-02 09:24:48 +08:00
wenjianzhang d1279e67fb Merge pull request #757 from NipGeihou/master
fix: 修复go generate命令不更新Swagger文档问题
2023-08-02 09:22:25 +08:00
wenjianzhang 37a5963cd6 perf👌: Optimize go warnings 2023-08-01 22:38:41 +08:00
wenjianzhang 69df1b3d34 perf👌: Remove unused attributes 2023-08-01 22:12:18 +08:00
wenjianzhang eae97f7a15 perf👌: update version 2023-08-01 22:08:47 +08:00
wenjianzhang ce0b5ff7bf perf👌: update version 2023-08-01 22:08:43 +08:00
NipGeihou 73118e49b9 fix: 修复go generate命令不更新Swagger文档问题
修复go generate不更新Swagger文档问题,并更新生成后文档文件
2023-06-20 00:37:59 +08:00
Vingurzhou 7b43982595 Update Makefile
fix(makefile): -installsuffix 参数没有指定后缀字符串。它被设定为空,这可能导致一些问题
2023-06-10 15:57:18 +08:00
wenjianzhang 31cd1ee768 Merge pull request #741 from wwhai/patch-2
fix: change 'os.Signal' channel to buffered
2023-05-14 11:04:04 +08:00
wenjianzhang 8df7551946 Merge pull request #740 from llussy/patch
fix setting.yml spelling
2023-05-14 11:03:47 +08:00
wenjianzhang 79c1295a70 Merge pull request #749 from sincatter/master_fix_pg_migrate
处理postgres迁移时insert提示类型不匹配问题
2023-05-14 11:03:07 +08:00
wenjianzhang 26d9a2e9e4 Merge pull request #747 from go-admin-team/dependabot/go_modules/golang.org/x/net-0.7.0
build(deps): bump golang.org/x/net from 0.0.0-20220722155237-a158d28d115b to 0.7.0
2023-05-13 15:38:46 +08:00
wenjianzhang b09b7b6ccc Merge pull request #729 from go-admin-team/dependabot/go_modules/github.com/prometheus/client_golang-1.11.1
build(deps): bump github.com/prometheus/client_golang from 1.11.0 to 1.11.1
2023-05-13 15:38:13 +08:00
dependabot[bot] ef6fdaa221 build(deps): bump golang.org/x/net
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.0.0-20220722155237-a158d28d115b to 0.7.0.
- [Commits](https://github.com/golang/net/commits/v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-13 07:38:07 +00:00
wenjianzhang 23c80f0217 Merge pull request #735 from go-admin-team/dependabot/go_modules/golang.org/x/text-0.3.8
build(deps): bump golang.org/x/text from 0.3.7 to 0.3.8
2023-05-13 15:37:14 +08:00
wenjianzhang 95dc699e2e Merge pull request #746 from haimait/fix_edit_master_role模块添加注释
fix_edit_master_role模块添加注释
2023-05-13 15:36:29 +08:00
wenjianzhang 74b7d62c75 处理postgres迁移时insert提示类型不匹配问题 2023-05-13 00:00:35 +08:00
wanghaima 68accd5448 fix_edit_master_role模块添加注释 2023-05-07 19:33:00 +08:00
wenjianzhang 3edbef8696 Merge pull request #745 from haimait/fix_edit_master_优化api筛选
优化API管理筛选
2023-05-07 19:22:23 +08:00
wanghaima 5527f6386a 优化API管理筛选 2023-05-07 18:47:36 +08:00
wwhai c48f70a7c6 fix: change 'os.Signal' channel to buffered 2023-05-04 23:25:43 +08:00
llussy a078e31664 fix setting.yml 2023-04-26 15:59:19 +08:00
wenjianzhang 04d2d7dde1 format🥚: Exclude empty permission identification 2023-04-19 18:50:35 +08:00
Akiraka b846053bea 恢复 common/middleware/demo.go 2023-04-14 19:51:48 +08:00
Akiraka a1a5634c4e 恢复修改 2023-04-14 19:51:22 +08:00
Akiraka 6036c6e4e3 接受参数位置错误 2023-04-14 19:07:01 +08:00
wenjianzhang 7d74e6f325 Merge pull request #732 from wenyoufu/master
【bug】修复普通用户只用查询权限时,无法修改个人信息(昵称、用户密码)的bug
2023-03-14 01:14:43 +08:00
dependabot[bot] 1bf9f74bdf build(deps): bump golang.org/x/text from 0.3.7 to 0.3.8
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.7 to 0.3.8.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.3.7...v0.3.8)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-23 00:08:28 +00:00
ford f61de5beeb 【bug】修复普通用户只用查询权限时,无法修改个人信息(昵称、用户密码)的bug 2023-02-17 19:24:50 +08:00
dependabot[bot] 79fb2d0bee build(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.11.0...v1.11.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-15 01:38:52 +00:00
wenjianzhang c973d6819c docs📝: update readme 2022-12-13 11:47:04 +08:00
wenjianzhang 3d8b879e64 docs📝: update readme 2022-12-13 11:45:28 +08:00
wenjianzhang ac971bda4b fix🐛: 忽略pkg包 2022-12-08 18:00:33 +08:00
wenjianzhang 44f62abbca fix🐛: 添加引用 2022-11-16 16:47:32 +08:00
wenjianzhang 7d7d8484d4 fix🐛: demo中间件添加环境判断 2022-11-16 12:05:33 +08:00
zhangwenjian c8b27492eb fix🐛: update sqlite3 configuration 2022-11-09 17:36:04 +08:00
zhangwenjian 783f79dcb6 Merge remote-tracking branch 'origin/master' 2022-11-09 17:35:31 +08:00
zhangwenjian ddd97d5a9e fix🐛: Adjust the demo environment configuration 2022-11-09 17:35:16 +08:00
wenjianzhang fa73c3d6b1 patch🚑: update restart 2022-11-03 14:55:02 +08:00
wenjianzhang 5b2f3e9316 Merge pull request #720 from ruishawn/dev
Docs: update README
2022-11-03 14:03:35 +08:00
zhangwenjian 42f3025217 fix🐛: Fix the problem that api saving fails when creating a new menu 2022-11-03 14:00:48 +08:00
wenjianzhang 5df43b4d11 docs📝: update readme 2022-11-02 10:18:32 +08:00
wenjianzhang eac1bf197e docs📝: update readme 2022-11-01 19:16:01 +08:00
wenjianzhang 5c834939af docs📝: update readme 2022-11-01 19:13:37 +08:00
zhangwenjian 642e86951b Merge branch 'master' of github.com:go-admin-team/go-admin 2022-11-01 16:58:11 +08:00
zhangwenjian bc42412e92 perf👌: 更新logo 2022-11-01 16:57:28 +08:00
wenjianzhang d9122d29cb docs📝: update readme 2022-11-01 16:54:19 +08:00
wenjianzhang b532ad994c docs📝: update readme 2022-11-01 16:53:42 +08:00
zhangwenjian b62fbc803c perf👌: 更新演示环境数据库名称 2022-11-01 15:02:36 +08:00
zhangwenjian ed8bac8a1b perf👌: 更新ci脚本 2022-11-01 15:00:41 +08:00
zhangwenjian d56142463a perf👌: 添加演示环境配置项 2022-11-01 14:58:18 +08:00
zhangwenjian f795a356e7 perf👌: 更新CI脚本中的分支 2022-11-01 14:14:27 +08:00
zhangwenjian a359c25e36 perf👌: 添加CI脚本 2022-11-01 14:13:53 +08:00
xiaobo 0ef422d309 fix: update README
Update README file: update dependencies before build.
2022-10-27 16:12:50 +08:00
wenjianzhang e0519a4d7e docs📝: update antd view url 2022-10-26 23:38:18 +08:00
wenjianzhang 5043ce5411 docs📝: update antd view url 2022-10-26 23:37:16 +08:00
wenjianzhang cc0cdc7d0e Merge pull request #710 from zyd/master
fix🐛:e.Log.Errorf("db error:%s", err)输出的err没有被赋值
2022-10-08 15:00:33 +08:00
zhaodongdong c004b3d333 fix🐛:e.Log.Errorf("db error:%s", err)输出的err没有被赋值 2022-09-14 17:42:09 +08:00
wenjianzhang 453dd65cb1 docs📝: update readme zh 2022-09-14 12:24:02 +08:00
wenjianzhang b55cbe1992 docs📝: Update readme 2022-09-14 12:23:06 +08:00
wenjianzhang b6e1b7b210 Merge pull request #708 from zyd/master
fix🐛:模板Update方法,err没有被赋值,返回的err永远是nil
2022-09-12 11:50:41 +08:00
zhaoyidong 3685510d25 fix🐛:模板Update方法,err没有被赋值,返回的err永远是nil 2022-09-07 20:08:17 +08:00
wenjianzhang d7e685536c Merge pull request #707 from zyd/master
fix🐛:排序参数必须用string接收
2022-09-07 09:53:24 +08:00
zhaoyidong 4df859a0ea fix🐛:排序参数必须用string接收
优化了代码生成的格式,最后的空行无法删除,删除之后}前面会增加空格
2022-09-06 14:10:27 +08:00
wenjianzhang 7d1b84e837 Merge pull request #706 from haimait/master-test
1. 修复日志创建时间筛选报错的bug.
2022-09-05 19:58:06 +08:00
wenjianzhang be1f0be9b2 Merge pull request #703 from quanbisen/master
修复优雅重启不生效
2022-09-05 19:56:46 +08:00
wanghaima 0f742900f5 1. 修复日志创建时间筛选报错的bug.
2. 修复日志里操作人,修改人,userAgent为空的bug.
3. 迁移表时,日志表添加字段注释.
4. sys_opera_log表oper_param字段类型改为text,解决字段长度报错的问题
2022-09-04 10:02:26 +08:00
quanbisen e6f4fac859 修复优雅重启不生效 2022-08-31 18:14:33 +08:00
wenjianzhang d39faa1aca docs📝: update readme zh 2022-08-25 14:25:07 +08:00
wenjianzhang c3fe13d9dd docs📝: Update readme 2022-08-25 14:24:05 +08:00
wenjianzhang 90db381b5a fix🐛: 修复侧边栏菜单排序问题 (690) 2022-08-25 14:18:02 +08:00
wenjianzhang 508137da4b docs📝: update README.md 2022-08-25 13:56:14 +08:00
wenjianzhang 695ac7b29b docs📝: Update README.Zh-cn.md 2022-08-25 13:55:11 +08:00
wenjianzhang c06df13a75 docs📝: update readme zh 2022-08-25 13:44:59 +08:00
wenjianzhang e663de3697 docs📝: update readme 2022-08-25 13:44:24 +08:00
wenjianzhang 10b4f03ff5 Merge pull request #701 from NaturalGao/natural
feat :update swag && add swag commond ssh
2022-08-25 13:42:42 +08:00
wenjianzhang c7a4434a0e docs📝: update readme 2022-08-25 13:40:24 +08:00
NaturalGao 1f8babd9e7 fix: fix sys_router && add swag commond 2022-08-25 01:19:55 +08:00
NaturalGao 8e8fe906fd perf: update swag 2022-08-25 01:00:57 +08:00
wenjianzhang 386d08a03f feat: update issue-labeled.yml 2022-08-24 16:27:19 +08:00
wenjianzhang af38e1694b feat: pr_cn.md 2022-08-24 11:26:01 +08:00
wenjianzhang 5609e004cc feat: PULL_REQUEST_TEMPLATE.md 2022-08-24 11:24:53 +08:00
wenjianzhang 5be468308a feat: update issue-labeled.yml 2022-08-23 11:55:03 +08:00
wenjianzhang 39702abfba feat: add issue-labeled.yml 2022-08-23 11:48:35 +08:00
wenjianzhang a97d86e801 feat: add issue-check-inactive.yml 2022-08-23 11:40:29 +08:00
wenjianzhang 75582539fe feat: add issue-close-require.yml 2022-08-23 11:39:19 +08:00
wenjianzhang 466723c55e Merge pull request #606 from npmmirror/master
Update https://registry.npm.taobao.org to https://registry.npmmirror.com
2022-08-23 11:30:20 +08:00
wenjianzhang 6b3b2125df docs📝: update README 2022-08-22 15:52:19 +08:00
wenjianzhang 8c8d268708 docs📝 update README 2022-08-22 15:51:33 +08:00
wenjianzhang 73ac7273a0 Merge pull request #695 from zyd/master
fix🐛:去除模板中的多余空格
2022-08-22 15:23:17 +08:00
wenjianzhang dcafcdc6ce Merge pull request #694 from infnan/master
处理postgre启动报错问题
2022-08-22 15:22:49 +08:00
zhaoyidong 5b1405391e fix🐛:去除模板中的多余空格 2022-08-18 18:36:50 +08:00
infnan 0993173b1f 处理postgre启动报错问题
Signed-off-by: infnan <38274826+infnan@users.noreply.github.com>
2022-08-18 16:50:54 +08:00
wenjianzhang 483ec2bf3e Create config.yml 2022-08-18 13:02:42 +08:00
wenjianzhang 60fe272ba0 refactor🎨: catch exception return error message
捕获runtime.Error异常,否则接口报错不返回任何信息
2022-08-18 10:14:12 +08:00
zhaoyidong 8df2e8190e 捕获runtime.Error异常,否则接口报错不返回任何信息
报错细节不应该隐藏,方便debug。500错误应该由前端统一处理,返回用户可读信息。
2022-08-18 10:02:00 +08:00
zhangwenjian 66c8eb5ee8 fix🐛: Fix create when creating a new create_by Problem with by value of 0 (#688) 2022-08-18 07:26:56 +08:00
zhangwenjian bb65e76219 fix🐛: Repair role creation prompt empty slice found (#687) 2022-08-18 07:19:24 +08:00
zhangwenjian a585e29073 Merge remote-tracking branch 'origin/master' 2022-08-18 06:57:20 +08:00
zhangwenjian 7655d0fd38 fix🐛: Repair document address (#692) 2022-08-18 06:57:02 +08:00
wenjianzhang 5bec640f20 fix🐛: Merge pull request #689 from zyd/master
修复模板get update delete错误
2022-08-16 11:58:33 +08:00
zhaoyidong 62d9084ee8 修复模板get update delete错误 2022-08-16 11:41:28 +08:00
wenjianzhang df8ab39aa0 config🔧: Merge pull request #685 from zyd/master
删除模板中间件重复初始化代码
2022-08-15 11:15:22 +08:00
wenjianzhang a7d3666811 config🔧: Merge pull request #686 from haimait/master_dev
编写dockerfile启动脚本
2022-08-15 11:15:07 +08:00
wanghaima ea9e3d2fe1 编写dockerfile启动脚本
编辑shell启动脚本
2022-08-14 23:32:58 +08:00
zhaoyidong f14085f3ee Merge pull request #1 from zyd/zyd-patch-1
删除模板中间件重复初始化代码
2022-08-13 17:39:14 +08:00
zhaoyidong e8b9db1df5 删除模板中间件重复初始化代码
go-admin app -n 创建目录,重复初始化会导致获取不到body中的参数
2022-08-13 17:37:55 +08:00
wenjianzhang dc625997c4 docs📝: Update README.Zh-cn.md 2022-08-10 01:00:34 +08:00
wenjianzhang 4f458591e8 docs📝: Update README.md 2022-08-10 00:47:42 +08:00
zhangwenjian 7a074d93cd config🔧: Modify the system default logo URL 2022-08-10 00:18:15 +08:00
wenjianzhang 9d1e1f6482 docs📝: Update README.Zh-cn.md 2022-08-09 23:24:35 +08:00
wenjianzhang 19153170bb docs📝: Update README.md 2022-08-09 23:23:18 +08:00
wenjianzhang 6bf774c463 fix🐛: fix rolemenu
fix🐛: fix rolemenu
2022-08-09 23:14:56 +08:00
wenjianzhang 0de5ba77aa docs📝: Update README.Zh-cn.md 2022-08-09 21:22:27 +08:00
wenjianzhang f4c0134d9c docs📝: Update Readme.md 2022-08-09 21:20:49 +08:00
wenjianzhang 2c5c1b69b4 docs📝: update readme 2022-08-09 20:46:37 +08:00
zhangwenjian d34a33b691 perf👌: update sqlite3 file 2022-08-09 18:21:40 +08:00
zhangwenjian 869394c898 perf👌: Remove caspin table from data migration 2022-08-09 18:21:00 +08:00
zhangwenjian b97bde11b2 perf👌: upgrade gorm,casbin,gin,uuid version 2022-08-09 18:19:02 +08:00
zhangwenjian 852cfa66e8 perf👌: remove casbin sys_ 2022-08-09 18:17:42 +08:00
zhangwenjian 289fbba8e0 perf👌: update casbin gorm adapter 2022-08-09 18:17:06 +08:00
zhangwenjian a6ffac657e fix🐛: Add MySQL judgment in data migration 2022-08-09 15:20:45 +08:00
zhangzhenlun 096663ac91 fix🐛: fix rolemenu 2022-08-09 14:26:43 +08:00
zhangwenjian d40a0c0837 fix🐛: fix github.com/alibaba/sentinel-golang middleware(#679) 2022-08-09 13:00:32 +08:00
zhangwenjian f94b437852 fix🐛: fix github.com/alibaba/sentinel-golang middleware 2022-08-09 12:59:31 +08:00
wenjianzhang 7e571b038f fix🐛: fix rolemenu 2022-08-09 10:18:21 +08:00
zhangwenjian 56968c0bd2 Merge remote-tracking branch 'origin/master' 2022-08-08 18:03:04 +08:00
zhangwenjian ef6b85faec config🔧: Modify the instruction createapp to app 2022-08-08 18:02:50 +08:00
zhangwenjian 0d47fb4e68 Merge branch 'master' of github.com:go-admin-team/go-admin 2022-08-08 16:28:34 +08:00
zhangwenjian 8bfee8af16 refactor🎨: 添加菜单paths默认数据 2022-08-08 16:28:26 +08:00
wenjianzhang 0fc7276ccd refactor🎨: set DB CHARSET utf8mb4 2022-08-08 11:39:11 +08:00
Vingurzhou 9a596397ca Update 1599190683659_tables.go 2022-08-08 11:28:55 +08:00
zhangwenjian 1bea64bb6d config🔧: set DB CHARSET utf8mb4(#674) 2022-08-08 10:16:43 +08:00
zhangwenjian 09bfdc3d39 config🔧: set DB CHARSET utf8mb4 2022-08-08 10:15:29 +08:00
wenjianzhang fe8b39691b patch🚑: go1.18 2022-08-08 10:00:24 +08:00
wenjianzhang 1465bfdf15 Merge branch 'master' into dev1.18 2022-08-08 09:59:57 +08:00
zhangwenjian bccbd67450 config🔧: update README 2022-08-08 09:42:31 +08:00
zhangwenjian 50a3b39666 config🔧: Modify the client IP acquisition method 2022-08-08 09:26:14 +08:00
zhangwenjian 087ba38c24 config🔧: update README.md 2022-08-08 09:22:36 +08:00
zhangwenjian 8a3c50ea1a config🔧: 修改actions配置文件 2022-08-07 21:52:02 +08:00
zhangwenjian 86187e4c79 fix🐛: 修复获取菜单接口menurole,数据不完整 (#676) 2022-08-07 21:49:24 +08:00
zhangwenjian 98c495abb1 refactor🎨: update readme 2022-08-07 20:43:34 +08:00
zhangwenjian c53c4fd9f8 refactor🎨: update readme 2022-08-07 20:42:24 +08:00
zhangwenjian b231705a67 Merge remote-tracking branch 'origin/master' 2022-08-04 17:25:17 +08:00
zhangwenjian 86f94a2cb9 refactor🎨: errors 添加go mod 2022-08-04 17:24:53 +08:00
zhangwenjian ef41e07550 docs📝: 添加开发环境要求 2022-08-04 17:24:07 +08:00
zhangwenjian 255c72d3f1 refactor🎨: update version 2022-07-29 18:51:55 +08:00
zhangwenjian 260eedfcc6 refactor🎨: 移除失效文档链接 2022-07-29 18:50:46 +08:00
zhangwenjian f43cd117e3 refactor🎨: 角色创建和更新后重新load policy策略 2022-07-29 18:47:12 +08:00
zhangwenjian 83b219458f fix🐛: 菜单中paths未设置问题修复 2022-07-29 18:44:22 +08:00
zhangwenjian 0f1b9369df fix🐛: 自定义错误中间件bug修复 2022-07-29 18:43:38 +08:00
zhangwenjian b31e1c0d58 feat: 升级go1.18 2022-07-27 22:27:38 +08:00
zhangwenjian 0122024789 feat: 修改版本号 2022-07-27 21:55:59 +08:00
zhangwenjian f469536174 fix🐛: 添加bcrypt包的引用 2022-07-27 21:49:24 +08:00
wenjianzhang 70bd8b26ad Merge pull request #673 from go-admin-team/dev
Dev
2022-07-27 21:36:11 +08:00
zhangwenjian dabc4d88b3 Merge remote-tracking branch 'origin/master' 2022-07-27 21:34:07 +08:00
wenjianzhang 214f90b366 Merge pull request #661 from wxxiong6/patch-1
fix UpdatePwd error
2022-07-27 21:20:07 +08:00
wenjianzhang 3c4cc054df Merge pull request #671 from Silicon-He/fix-readme-cgo-url
Fix readme cgo url
2022-07-27 21:19:29 +08:00
wenjianzhang 4d329287ce Merge pull request #664 from zhouxixi-dev/dev
bugfix: https://github.com/go-admin-team/go-admin/issues/539
2022-07-27 21:17:43 +08:00
siliconhe 866714d75e update doc url of cgo-issue 2022-07-17 21:14:26 +08:00
lwnmengjing b268d03e30 💚 update workflow 2022-07-12 11:54:24 +08:00
lwnmengjing 953d3b4135 🐛 fix: delete pkg error package 2022-07-12 10:55:05 +08:00
zhouxixi-dev 0d6b347d7e bugfix: https://github.com/go-admin-team/go-admin/issues/539 修复角色新增、修改时,sys_casbin_rule表drop,然后重新create的问题 2022-06-23 15:47:55 +08:00
wxxiong6 a19622f6ac fix UpdatePwd error
fix UpdatePwd error
2022-06-13 23:36:12 +08:00
zhangwenjian 45c8737601 refactor🎨: 引入github.com/pkg/errors 2022-06-05 11:12:19 +08:00
wenjianzhang 4925383f5e Merge pull request #593 from ziux/ziux
fix bug
2022-06-05 11:08:09 +08:00
wenjianzhang 62ab985050 Merge pull request #552 from wkf928592/wkf928592-patch-1
fix: arm32位系统环境下使用migrate迁移功能时,版本号作为整型处理会出现内存溢出的问题
2022-06-05 10:36:27 +08:00
wenjianzhang 50f14f7658 Merge branch 'dev' into wkf928592-patch-1 2022-06-05 10:36:20 +08:00
wenjianzhang 1cd31079d7 Merge pull request #634 from defool/bugfix/throw_error_on_migrate
Throw error if migrate failed
2022-06-05 10:33:08 +08:00
wenjianzhang 46b62a9809 Merge pull request #655 from stephenzhang0713/dev
Fix: Unable to load config file to Docker container
2022-06-05 10:31:54 +08:00
Han Zhang 8f70a1dca4 fix🐛: Fix Dockerfile to load config file
fix🐛: Fix Dockerfile to load config file
2022-06-03 20:38:54 +08:00
zhangwenjian 937b62e641 添加errors包 2022-05-29 12:58:53 +08:00
wenjianzhang 3873342f6c Merge pull request #650 from go-admin-team/dev
refactor🎨: 修复问题
2022-05-28 00:40:06 +08:00
zhangwenjian 717903a2b6 fix🐛: 修复关闭中的job能够启动问题(#638) 2022-05-28 00:34:08 +08:00
zhangwenjian 88030e301a patch🚑: 更新版本信息 2022-05-28 00:17:35 +08:00
zhangwenjian cd0792e3d2 refactor🎨: 更新readme(#623) 2022-05-28 00:14:12 +08:00
zhangwenjian 6ed2fcbf6c fix🐛: 添加roleKey验证(#649) 2022-05-27 22:47:57 +08:00
kaiyuan eb33515e29 throw error if migrate failed 2022-04-08 10:32:07 +08:00
wenjianzhang bf93b86bd0 Merge pull request #629 from go-admin-team/dev
docs📝: update readme
2022-04-01 14:14:01 +08:00
wenjianzhang c41672c21a docs📝: update readme 2022-03-31 15:08:49 +08:00
wenjianzhang 8716b073df Merge pull request #628 from go-admin-team/dev
docs📝:  update readme
2022-03-31 14:13:26 +08:00
wenjianzhang 638bab3c9d docs📝: update readme 2022-03-31 14:01:15 +08:00
wenjianzhang c5d7a8c740 fix🐛: Fix password reset
fix🐛: Fix password reset
2022-03-15 11:33:21 +08:00
wenjianzhang b030be8f80 fix🐛: Fix password reset 2022-03-12 13:00:19 +08:00
wenjianzhang 1fe19d1c3b patch🚑: dev merge
patch🚑:  dev merge
2022-03-05 11:54:10 +08:00
wenjianzhang 1508e850fe Merge branch 'master' into dev 2022-03-05 11:52:44 +08:00
zhangwenjian 81bc15d77c config🔧: go-admin version info 2022-03-05 11:49:56 +08:00
zhangwenjian 24adca55e4 feat: added file update sdk;kodo、obs 2022-03-05 11:44:23 +08:00
wenjianzhang 9c8974a26e fix🐛: 修复前端设置数据权限不生效问题
fix🐛: 修复前端设置数据权限不生效问题
2022-03-05 11:23:41 +08:00
wenjianzhang 01e8984b79 fix🐛: Fix readme 404 link.
fix🐛:  Fix readme 404 link.
2022-03-05 11:13:08 +08:00
wenjianzhang 7f1aa89539 fix🐛: Fix the newline problem in time package in code generation
Fix gen code problem
2022-03-05 11:11:34 +08:00
zhangwenjian 4876fc0aa1 fix🐛: Fix password reset caused by modifying user information 2022-03-05 11:00:52 +08:00
zhangwenjian f998d20a86 feat: added obs,kodo 2022-02-21 18:07:37 +08:00
zhangwenjian cdb5faf043 refactor🎨: upgrade OXS interface 2022-02-21 18:06:11 +08:00
zhangwenjian dfaa2ff51e test: added oss test 2022-02-21 18:04:46 +08:00
zhangwenjian 8754ff8147 refactor🎨: upgrade oss 2022-02-21 18:04:18 +08:00
zhangwenjian cca84c3c21 docs📝: update License Copyright 2022-02-21 17:56:12 +08:00
wenjianzhang 72391f0201 Merge pull request #598 from go-admin-team/dev
fix🐛: fix monitor macos env error (#605)
2022-02-13 00:54:52 +08:00
zhangwenjian 39e26d738c docs📝: update version 2.0.9 2022-02-13 00:31:46 +08:00
zhangwenjian f114424079 fix🐛: fix monitor macos env error 2022-02-13 00:23:20 +08:00
NPM Mirror Bot 98b60f0564 update https://registry.npm.taobao.org to https://registry.npmmirror.com 2022-02-12 05:56:42 +00:00
wenjianzhang d02b52f383 feat: 添加sqlserver支持 2022-02-08 18:41:09 +08:00
horizonzy b52da434bb fix code gen problem. 2022-01-31 13:00:21 +08:00
horizonzy 3cfa7a2767 fix code gen problem. 2022-01-31 12:00:26 +08:00
horizonzy 054199d1e4 fix 404 link. 2022-01-30 17:19:50 +08:00
zhangwenjian 02b62a288d refactor🎨: 删除历史的sqlite文件 2022-01-22 23:05:38 +08:00
zhangwenjian 24908a8732 refactor🎨: 添加error判断返回 2022-01-22 23:03:01 +08:00
wenjianzhang 26ee7b7985 refactor🎨: 清空sqlite数据库文件 2022-01-22 21:49:42 +08:00
wenjianzhang b76db48112 refactor🎨: 修正针对sqlite3的事务问题 2022-01-22 21:49:11 +08:00
wenjianzhang e2c5075319 v2.0.8
1、修改sqplite的支持
2、修正已知问题
2022-01-22 20:03:21 +08:00
wenjianzhang e9f36e74ca refactor🎨: 修改版本号 2022-01-22 19:44:36 +08:00
wenjianzhang 0b73bd7b25 refactor🎨: 修改sqplite的支持 2022-01-22 19:37:56 +08:00
yangyu ccccab3104 fix bug 2022-01-12 17:10:52 +08:00
wenjianzhang ae8e32d806 Update README.Zh-cn.md 2022-01-10 13:18:59 +08:00
wenjianzhang fd2709affa Update README.md 2022-01-10 13:18:23 +08:00
wenjianzhang 34b3395d15 Update README.md 2022-01-10 13:17:28 +08:00
inits abdc80b756 修复前端设置数据权限不生效问题 2022-01-10 10:21:10 +08:00
lwnmengjing 492ac31973 Merge pull request #580 from go-admin-team/dev
push docker
2021-12-08 11:09:01 +08:00
linwenxiang 3ac1878c3c perf performance docker build 2021-12-07 23:37:33 +08:00
linwenxiang 0cc27355f7 fix 🐛 push to gihub 2021-12-07 23:24:19 +08:00
linwenxiang 562f761807 feat add dev to ci 2021-12-07 22:47:44 +08:00
linwenxiang 88e4b37c03 feat push docker to github 2021-12-07 22:44:39 +08:00
wenjianzhang b73d88d6cf Merge pull request #564 from go-admin-team/dev
merge: 修正数据初始化的部分问题
2021-10-22 12:15:05 +08:00
wenjianzhang 443c30d48c Update 1599190683659_tables.go 2021-10-22 12:05:39 +08:00
wenjianzhang 64cbf31184 Update db.sql 2021-10-22 12:04:48 +08:00
wkf928592 ce9d9bd3ec fix:在32位系统中做迁移时,版本号作为整型处理会造成内存溢出的问题
修改版本号作为字符串处理
2021-09-10 10:19:33 +08:00
linwenxiang 57330784ac feat mirror to gitlab 2021-09-07 21:45:25 +08:00
lwnmengjing 85e1c6fe54 Merge branch 'dev' 2021-09-07 11:21:33 +08:00
lwnmengjing 7ca776bfab 💚 修复流水线CI bug 2021-09-07 11:20:55 +08:00
linwenxiang 082c369d41 feat 同步代码到gitee 2021-09-06 21:51:07 +08:00
lwnmengjing 0ac9f41e1a Merge pull request #549 from go-admin-team/dev
fix 🐛 gcc强依赖问题修复
2021-09-02 20:38:36 +08:00
linwenxiang 16701d38a6 feat 增加release pipeline 2021-09-02 20:31:00 +08:00
linwenxiang 30eb280698 fix 🐛 gcc强依赖问题修复 2021-09-02 20:21:18 +08:00
wenjianzhang 974a8096ca Merge pull request #546 from go-admin-team/dev
Dev
2021-08-21 14:20:26 +08:00
wenjianzhang bb83a97613 refactor🎨: 修改post接口文档 2021-08-20 18:28:04 +08:00
wenjianzhang 8baae5e712 fix🐛: 修复jwt密钥引用错误问题(#545) 2021-08-20 18:27:29 +08:00
wenjianzhang 49e4c19cbb Merge pull request #544 from go-admin-team/dev
Dev
2021-08-19 19:36:42 +08:00
wenjianzhang 6f67628012 Merge branch 'dev' of github.com:go-admin-team/go-admin into dev 2021-08-19 19:28:04 +08:00
wenjianzhang 8a1573cc14 docs📝: 更新2.0.6 2021-08-19 19:27:55 +08:00
wenjianzhang 03b916ef8a Merge pull request #543 from go-admin-team/dev
Dev
2021-08-19 19:26:31 +08:00
wenjianzhang 9d25648c1f Merge pull request #540 from ninstein/patch-8
BUGFIX:角色状态修改异常修复
2021-08-19 19:13:16 +08:00
wenjianzhang aa6c3df892 Merge pull request #542 from go-admin-team/dev
Dev
2021-08-19 19:12:32 +08:00
wenjianzhang bdaa6e0db0 refactor🎨: 升级包go-admin-core v1.3.7和go-admin-core/sdk v1.3.7至v1.3.8 2021-08-19 19:10:32 +08:00
wenjianzhang 4740a39808 refactor🎨: 删除移除功能的数据初始化 2021-08-19 19:08:47 +08:00
wenjianzhang 386c620b48 refactor🎨: 优化角色修改时循环AddNamedPolicy 2021-08-19 19:04:03 +08:00
wenjianzhang 2441412714 fix🐛: 修复参数验证信息 2021-08-19 19:03:19 +08:00
ninstein 9db940150a BUGFIX:角色状态修改异常修复
切换角色状态时参数传递丢失,导致切换异常新增了一条空记录
2021-08-18 14:45:22 +08:00
wenjianzhang 8c5639af53 Merge pull request #535 from go-admin-team/dev
Dev
2021-08-13 21:17:00 +08:00
wenjianzhang b4a6f82f5f Merge pull request #534 from appleboy/patch
chore: upgrade gin to v1.7.3
2021-08-13 21:15:58 +08:00
Bo-Yi Wu 7dd62a4cf8 chore: upgrade gin to v1.7.3
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2021-08-13 20:53:37 +08:00
wenjianzhang 095ed7c2fd Merge pull request #529 from go-admin-team/dev
Dev
2021-08-10 16:39:54 +08:00
zhangwenjian 76567eea84 docs📝: 更新2.0.5 2021-08-10 14:58:55 +08:00
zhangwenjian 5a65fcd477 fix🐛: 修复菜单树 2021-08-10 14:58:04 +08:00
wenjianzhang 27b0e1a07a Merge pull request #528 from go-admin-team/dev
Dev
2021-08-10 03:55:34 +08:00
zhangwenjian 3e20e93797 fix🐛: 修复菜单编辑未赋权接口列表 2021-08-10 03:47:31 +08:00
zhangwenjian 5d3b1c3d0f docs📝: 更新2.0.4 2021-08-10 03:37:02 +08:00
zhangwenjian b5a57e6dd9 refactor🎨: 优化生成功能的修改和删除询问提示 2021-08-10 03:36:20 +08:00
zhangwenjian 2decf43b4c fix🐛: 统一生成后的路由 2021-08-10 03:35:36 +08:00
wenjianzhang 7dd3e2b27e Merge pull request #525 from go-admin-team/dev
Dev
2021-08-06 10:49:41 +08:00
zhangwenjian 325c91989c refactor🎨: api自动添加不设置默认类型 2021-08-06 10:35:09 +08:00
wenjianzhang 96250bafb1 Merge pull request #521 from qliang/master
完善:接口检查新增记录-根据接口注释补充接口名称信息
2021-08-06 10:28:07 +08:00
wenjianzhang fff795ce5a Merge pull request #524 from go-admin-team/dev
fix🐛: 修复代码生成字典的问题 (#523  #517)
2021-08-06 10:27:32 +08:00
zhangwenjian 9887250407 docs📝: 更新2.0.3 2021-08-06 10:03:44 +08:00
zhangwenjian e42191c6af fix🐛: 修复代码生成字典的问题 (#523 #517) 2021-08-06 10:00:17 +08:00
lq adce44dc1f 完善:接口检查新增记录-根据接口注释补充接口名称信息 2021-08-03 17:24:58 +08:00
wenjianzhang a70ee44466 Merge pull request #511 from go-admin-team/dev
patch🚑:  merge dev
2021-07-28 10:00:12 +08:00
zhangwenjian fd0fa49f1c docs📝: 更新2.0.2 2021-07-28 09:06:23 +08:00
zhangwenjian 10491f9745 fix🐛: 修复删除部门的问题 (#510) 2021-07-28 08:53:18 +08:00
zhangwenjian 981313c0e2 fix🐛: 修复创建用户时的问题 ( #506) 2021-07-28 08:45:39 +08:00
zhangwenjian 6b88c9a004 fix🐛: 更新接口文档注释 (#507) 2021-07-27 19:16:47 +08:00
wenjianzhang 25395b5006 Merge pull request #504 from go-admin-team/dev
1. 修复菜单的目录(#500)
1. 调整字段判断逻辑
2021-07-23 00:43:34 +08:00
zhangwenjian 2f516b49cf refactor🎨: 调整字段判断逻辑 2021-07-23 00:33:39 +08:00
zhangwenjian 41ff26edc8 fix🐛: 修复菜单的目录(#500) 2021-07-23 00:33:00 +08:00
wenjianzhang a1c6f586cf Merge pull request #503 from go-admin-team/dev
fix🐛: 修复createapp时的问题(#493)
2021-07-22 23:11:56 +08:00
zhangwenjian 8b01126e0f fix🐛: 修复createapp时的问题(#493) 2021-07-22 23:03:44 +08:00
wenjianzhang e59d40af21 Merge pull request #495 from go-admin-team/dev
Dev
2021-07-22 22:22:20 +08:00
wenjianzhang 6b476bfab7 Update go.mod 2021-07-18 22:29:34 +08:00
wenjianzhang 12429d4585 Merge pull request #489 from Cassuis/dev
fix:修复createapp未初始化导致无法创建app以及修改资本资料导致密码重复加密问题
2021-07-16 20:52:40 +08:00
Vincent 6fe2edbe89 fix:修复修改基本资料导致密码重复加密问题 2021-07-15 11:13:37 +08:00
Vincent cee6bd6abd fix:修复createapp未初始化导致无法创建app的问题 2021-07-15 10:21:26 +08:00
zhangwenjian 4ac3350920 refactor🎨: 部分函数名称优化 2021-07-15 00:53:43 +08:00
zhangwenjian a45113258c refactor🎨: update request mode name 2021-07-15 00:43:06 +08:00
zhangwenjian 4a2659573b docs📝: 用户接口文档 2021-07-14 22:36:03 +08:00
zhangwenjian c74080664f refactor🎨: update version 2021-07-14 22:24:26 +08:00
wenjianzhang 84e06395a5 Merge pull request #487 from go-admin-team/dev
Dev
2021-07-14 16:24:46 +08:00
zhangwenjian 411b85afcd publish🚀: 2.0.0 2021-07-14 11:49:54 +08:00
zhangwenjian 57d128144d feat: Add the createapp command 2021-07-14 11:48:40 +08:00
wenjianzhang a7fa7e079b Merge pull request #486 from go-admin-team/dev
Dev
2021-07-14 11:40:05 +08:00
zhangwenjian 2781e413dc refactor🎨: 修改版本号 2021-07-14 11:16:27 +08:00
zhangwenjian 41d8daac97 fix🐛: 修改用户其它信息导致密码被置空,数据权限 #484 2021-07-14 11:13:26 +08:00
zhangwenjian dd22d55ee7 refactor🎨: request name cancel 2021-07-14 09:14:44 +08:00
zhangwenjian 32a1bd2511 refactor🎨: 升级gin和gorm版本 2021-07-05 00:31:31 +08:00
zhangwenjian 1130d20f14 refactor🎨: dto》request 2021-07-05 00:03:38 +08:00
wenjianzhang 90b17e995a Merge pull request #478 from go-admin-team/dev
Dev
2021-07-04 23:33:55 +08:00
zhangwenjian 1c41b0ec72 refactor🎨: 操作log dto模型名称修改 2021-07-04 23:26:43 +08:00
zhangwenjian 3f90605589 refactor🎨: 操作log添加字符限制 2021-07-04 23:13:14 +08:00
zhangwenjian c1347fbb5d refactor🎨: 升级依赖关系 2021-07-04 23:12:21 +08:00
zhangwenjian 0d1cb2ee33 docs📝: 升级qq群至2000人 2021-07-04 23:11:58 +08:00
zhangwenjian b6be297a1e refactor🎨: 添加默认demo代码生成表 2021-07-04 13:43:32 +08:00
zhangwenjian b9b3cbee93 refactor🎨: 调整登陆日志和api和操作日志模块 2021-07-04 13:43:11 +08:00
zhangwenjian 37d318b4d9 feat: vue-cli@3 升级为 vue-cli@4、Change Node Sass to Dart Sass、代码生成工具
1. vue-cli@3 升级为 vue-cli@4
2. Change Node Sass to Dart Sass
3. 代码生成工具
2021-07-04 05:46:20 +08:00
wenjianzhang 5368cfbcb8 Merge pull request #475 from G-Akiraka/patch-4
增加磁盘列表主机名称与当前时间
2021-07-02 22:26:09 +08:00
wenjianzhang 72a4ba077c Merge pull request #472 from G-Akiraka/patch-1
Update settings.yml
2021-07-02 22:25:53 +08:00
wenjianzhang fba03625d0 Merge pull request #473 from G-Akiraka/patch-2
翻译错误,管理员管理应该是用户管理
2021-07-02 22:24:25 +08:00
G-Akiraka 007807e776 增加磁盘列表主机名称与当前时间
上一个pr提交作废
2021-07-02 10:06:48 +08:00
G-Akiraka f2ae95d932 翻译错误,管理员管理应该是用户管理 2021-07-02 09:09:20 +08:00
G-Akiraka 25d7323ed1 Update settings.yml 2021-07-02 09:05:25 +08:00
wenjianzhang f7e737534d Merge pull request #471 from go-admin-team/dev
Dev
2021-07-01 23:13:03 +08:00
zhangwenjian 760c6b2814 refactor🎨: 修改版本号 2021-07-01 22:25:43 +08:00
zhangwenjian d0f49ef8c7 refactor🎨: 修改错误信息提示(#165) 2021-07-01 22:24:32 +08:00
wenjianzhang 1295b1fd35 Merge pull request #469 from go-admin-team/dev
DEV (#468)
2021-06-30 22:48:45 +08:00
zhangwenjian eb8062d19b refactor🎨: 生成逻辑调整 2021-06-30 22:36:26 +08:00
zhangwenjian d82129d364 refactor🎨: 代码生成模版升级 2021-06-30 22:35:49 +08:00
zhangwenjian 16923017b2 refactor🎨: 配置文件默认使用memory cache 2021-06-30 22:35:29 +08:00
zhangwenjian f4396e7e83 fix🐛: 用户修改头像接口入参模型分离(#468) 2021-06-30 18:55:16 +08:00
wenjianzhang acee466fc8 Merge pull request #467 from go-admin-team/dev
Dev(#457)
2021-06-30 02:04:10 +08:00
zhangwenjian 862f24d8a7 refactor🎨: 移除服务管理 2021-06-30 01:52:47 +08:00
wenjianzhang e2cb033670 refactor🎨: 增加server manager,提升服务启动流程规范
feature  增加server manager,提升服务启动流程规范
2021-06-29 16:58:50 +08:00
zhangwenjian 029c505501 refactor🎨: 修改包引用 2021-06-29 16:46:00 +08:00
zhangwenjian dd5f0c52fb refactor🎨: 修改错误信息提示 2021-06-29 16:33:03 +08:00
zhangwenjian f80e64688e refactor🎨: 修改请求参数命名 2021-06-29 16:32:44 +08:00
zhangwenjian 3fd34db3c1 docs📝: 修改接口文档 2021-06-29 16:30:47 +08:00
zhangwenjian 0607d462d0 docs📝: 修改config接口文档 2021-06-29 16:28:08 +08:00
zhangwenjian 7f8c302405 refactor🎨: 调整index to go-admin 接口 2021-06-29 16:27:10 +08:00
zhangwenjian c82c58443c refactor🎨: 验证码文档修改,去掉token验证 2021-06-29 16:26:27 +08:00
zhangwenjian 9591be883f refactor🎨:登陆模块接口文档整理 2021-06-25 11:34:26 +08:00
zhangwenjian acdcd0c867 refactor🎨:接口文档重新生成 2021-06-25 11:33:55 +08:00
zhangwenjian 0f623521c4 refactor🎨:代码生成功能迁移 2021-06-25 11:33:41 +08:00
wenjianzhang 91b93a3f48 refactor🎨:模版升级 2021-06-24 20:21:40 +08:00
wenjianzhang 32d123eb89 refactor🎨:格式化函数名称 2021-06-24 20:18:20 +08:00
wenjianzhang 2025809d91 refactor🎨:修改Syspost模块功能 2021-06-24 20:17:58 +08:00
wenjianzhang d552875e8a Merge pull request #461 from go-admin-team/dev
Dev
2021-06-23 12:21:13 +08:00
wenjianzhang 6994857f4b Merge pull request #460 from Cassuis/dev
bugfix:修正用户修改密码put接口url错误导致的无法修改密码问题,修正初始化SQL异常导致部分表无缺省参数问题
2021-06-23 12:08:37 +08:00
Vincent 7ac941c1d0 bugfix:
1.修正用户修改密码put接口url错误导致的无法修改密码问题
2.修正初始化SQL异常导致部分表无缺省参数问题
2021-06-23 09:46:51 +08:00
wenjianzhang af68778436 Merge pull request #455 from go-admin-team/dev
docs📝:  更新swagger文档
2021-06-20 00:59:39 +08:00
zhangwenjian 1ab11c46ba docs📝: 更新swagger文档 2021-06-20 00:51:55 +08:00
wenjianzhang add355637d Merge pull request #450 from go-admin-team/dev
merge Dev
2021-06-20 00:51:05 +08:00
zhangwenjian 65380695f2 refactor🎨:格式化函数名称 2021-06-20 00:43:17 +08:00
zhangwenjian a6116ede02 refactor🎨: 修改字典类型status为int 2021-06-20 00:39:43 +08:00
zhangwenjian dafa627e31 refactor🎨:升级依赖版本 2021-06-20 00:15:54 +08:00
zhangwenjian 993115c8ae fix🐛: 修复获取部门数据查询参数问题 2021-06-20 00:15:34 +08:00
zhangwenjian b97b3bc6d8 refactor🎨:部门创建添加事务,已经修改status字段为int类型 2021-06-20 00:14:45 +08:00
zhangwenjian 636294b6f5 refactor🎨: 岗位删除修改为data传值方式 2021-06-20 00:13:17 +08:00
zhangwenjian 37fe63126c fix🐛: 修复2.0 字典类型删除接口500并没有提示消息 (#452) 2021-06-18 22:24:10 +08:00
zhangwenjian 9ace1d8201 fix🐛: 修复2.0 字典数据删除404 (#451) 2021-06-18 22:23:37 +08:00
zhangwenjian 25472887c9 refactor🎨:移除内容管理、行政区管理和资源管理 2021-06-18 21:34:57 +08:00
zhangwenjian 5e7d2614b3 refactor🎨:修改版本号 2021-06-17 22:14:07 +08:00
zhangwenjian 484246e146 refactor🎨:更新数据初始化sql 2021-06-17 22:10:10 +08:00
zhangwenjian 65fb965f66 refactor🎨:修改行政区数据sql 2021-06-17 21:46:42 +08:00
zhangwenjian 493d714723 refactor🎨:修改行政区数据sql 2021-06-17 21:46:19 +08:00
zhangwenjian a911e73238 refactor🎨:调整行政区命名 2021-06-17 21:45:37 +08:00
zhangwenjian dff133bbbc refactor🎨:注释数据权限控制历史版本方法 2021-06-17 21:11:58 +08:00
zhangwenjian 60d9c59544 refactor🎨:删除历史版本菜单、角色、角色部门关系、角色菜单关系业务 2021-06-17 21:11:09 +08:00
zhangwenjian 185df737c8 refactor🎨:调整管理员和字典相关结构体字段顺序 2021-06-17 21:10:12 +08:00
zhangwenjian a0b7d9969a Revert "refactor🎨:注释未使用的对象"
This reverts commit 3358eeb54c.
2021-06-17 11:28:51 +08:00
zhangwenjian 3358eeb54c refactor🎨:注释未使用的对象 2021-06-17 11:28:37 +08:00
wenjianzhang d32751ea5d refactor🎨:文件上传修改为本地路径 2021-06-16 18:34:51 +08:00
wenjianzhang 8f637e02d0 refactor🎨:菜单、角色、用户模块调整 2021-06-16 18:34:25 +08:00
wenjianzhang e4ffd1df14 refactor🎨:注释返回数据记录 2021-06-16 18:33:56 +08:00
wenjianzhang 869bea2163 refactor🎨:系统监控接口 2021-06-16 18:33:14 +08:00
linwenxiang 7e9919e3ae feature 增加server manager,提升服务启动流程规范 2021-06-16 10:13:26 +08:00
wenjianzhang 5afe67bd1b refactor🎨:更新角色模块 2021-06-15 19:03:37 +08:00
wenjianzhang 13f09d6059 refactor🎨:数据迁移结构体重命名 2021-06-15 19:02:57 +08:00
wenjianzhang 1c9d2075d7 refactor🎨:调整接口和行政区结构体 2021-06-15 17:54:46 +08:00
wenjianzhang 3a688c06a1 refactor🎨:初始化脚本针对关键字添加引号 2021-06-15 17:54:04 +08:00
wenjianzhang 2d4b4d617d Merge branch 'dev' 2021-06-15 12:28:02 +08:00
wenjianzhang 9fb23b45cd Merge branch 'dev' 2021-06-15 12:25:46 +08:00
wenjianzhang af7a3e99a8 efactor🎨:修改行政区域数据结构
主键不需要自增
2021-06-15 12:22:30 +08:00
wenjianzhang 992d892523 refactor🎨:补充初始化数据 2021-06-15 12:16:44 +08:00
zhangwenjian 7699d8cf42 refactor🎨: 调整行政区管理相关 2021-06-15 09:29:35 +08:00
zhangwenjian f81c9c2ca0 refactor🎨: 调整删除时获取使用函数获取id 2021-06-15 09:28:58 +08:00
zhangwenjian 76351fe685 refactor🎨: 修正结构定义方法和绑定数据类型 2021-06-15 09:28:04 +08:00
zhangwenjian f0e8f18c6d refactor🎨: 更新客户端ip获取方法 2021-06-14 20:08:30 +08:00
zhangwenjian 880a3700d1 refactor🎨: 添加日志排序 2021-06-14 20:07:48 +08:00
zhangwenjian 273f2ba8b0 fix🐛: merge 的遗漏 2021-06-13 23:20:30 +08:00
zhangwenjian a3507998f7 Merge branch 'dev' 2021-06-13 23:14:34 +08:00
zhangwenjian 67d393a222 docs📝: update readme & dockfile 2021-06-13 23:09:11 +08:00
zhangwenjian 372248c819 Merge branch 'dev' 2021-06-13 23:07:37 +08:00
zhangwenjian b837489af0 docs📝: 更新readme 2021-06-13 21:52:15 +08:00
zhangwenjian e7223be040 refactor🎨: 升级依赖 2021-06-13 21:48:51 +08:00
zhangwenjian e37743733d refactor🎨: 更新初始化数据sql 2021-06-13 21:48:03 +08:00
zhangwenjian 58a9b00120 refactor🎨: 注释演示环境代码 2021-06-13 21:29:32 +08:00
zhangwenjian d8a627f880 refactor🎨: 优化资源管理的路由函数名称 2021-06-13 21:28:14 +08:00
zhangwenjian df46f86130 fix🐛: 修复菜单更新是不能更新绑定api的问题 2021-06-13 21:27:14 +08:00
zhangwenjian c98dd649eb refactor🎨: 添加更新handle 2021-06-13 21:25:47 +08:00
zhangwenjian da15a2d3bb refactor🎨: 调整SysChinaAreaData Api的函数名称以及路由引用 2021-06-13 21:24:09 +08:00
zhangwenjian 00c87d7c80 feat: api rabc检测时将排除列表中的path剔除不在参与验证 2021-06-13 21:22:55 +08:00
zhangwenjian 7c8c285fb8 feat: 添加排除路由列表 2021-06-13 21:20:44 +08:00
zhangwenjian e0cf7af7a9 refactor🎨: 调整客户端ip获取方法以及日志中对应位置更新 2021-06-13 21:20:07 +08:00
zhangwenjian ed52efaa44 refactor🎨: 修改数据字典路由注册 2021-06-13 21:16:55 +08:00
zhangwenjian 4a2aa02210 refactor🎨: 批量修改通过id获取详情返回消息 2021-06-13 21:14:17 +08:00
wenjianzhang cc0dab3cd0 refactor🎨: 开放接口无需认证 2021-06-11 17:30:17 +08:00
zhangwenjian 7b6e57b8dd Merge branch 'dev' of https://github.com/go-admin-team/go-admin into dev 2021-06-11 16:05:06 +08:00
zhangwenjian 325b2cc0a2 refactor🎨: demo环境中间件 2021-06-11 16:05:03 +08:00
wenjianzhang 52f45e362b Merge branch 'dev' of github.com:go-admin-team/go-admin into dev 2021-06-11 15:26:41 +08:00
wenjianzhang 1391366ece refactor🎨: 添加行政区模型 2021-06-11 15:26:35 +08:00
zhangwenjian 40668581b8 refactor🎨: 添加登陆验证 2021-06-11 09:26:22 +08:00
zhangwenjian 261e448577 refactor🎨: 操作日志去除菜单关联查询 2021-06-11 09:26:00 +08:00
zhangwenjian 7c7cd7ef7e refactor🎨: engine 初始化调整 2021-06-11 09:25:35 +08:00
zhangwenjian 860226ab41 refactor🎨: engine 初始化调整 2021-06-11 09:25:10 +08:00
zhangwenjian 898b1ea1e6 refactor🎨: 预览环境打包使用 2021-06-11 09:24:32 +08:00
linwenxiang f4d63c57e9 bugfix 🐛 提交遗漏代码 2021-06-11 09:20:27 +08:00
linwenxiang 3aa64d107b feature 优化setup 2021-06-10 17:15:13 +08:00
zhangwenjian 0e3e733745 Merge branch 'dev' of https://github.com/go-admin-team/go-admin into dev 2021-06-10 14:47:02 +08:00
zhangwenjian e6384de265 refactor🎨: 升级依赖 2021-06-10 14:46:29 +08:00
linwenxiang b9e8759ef1 feature 支持大文件分割配置 2021-06-10 11:54:02 +08:00
zhangwenjian ff5d498c53 refactor🎨: job和代码生成模块调整 2021-06-10 11:25:39 +08:00
zhangwenjian a8820a0aac refactor🎨: 菜单以及菜单角色模块调整 2021-06-10 11:24:59 +08:00
zhangwenjian 24c78519ed refactor🎨: 数据迁移模型调整 2021-06-10 11:24:35 +08:00
zhangwenjian 49c6febdf0 refactor🎨: 日志中的位置获取函数添加key传入 2021-06-10 11:24:17 +08:00
zhangwenjian 3d620d0149 refactor🎨: 添加高德地图key自定义扩展配置 2021-06-10 11:22:53 +08:00
zhangwenjian 352224b994 refactor🎨: 修改数据迁移脚本 2021-06-10 11:22:23 +08:00
zhangwenjian 897b9e6919 Merge branch 'dev' of https://github.com/go-admin-team/go-admin into dev 2021-06-09 21:36:57 +08:00
zhangwenjian 1b579fb814 feat: 添加修改角色状态接口 2021-06-09 21:31:31 +08:00
linwenxiang ab9020e7c7 feature gin请求纳入性能指标 2021-06-09 21:21:45 +08:00
wenjianzhang 54e116818b refactor🎨: 数据初始化 2021-06-09 18:30:47 +08:00
wenjianzhang 6a31ac916a refactor🎨: 移除v1版本中的数据迁移脚本 2021-06-09 18:25:00 +08:00
linwenxiang f35a808975 perf 优化配置文件加载 2021-06-09 16:30:24 +08:00
linwenxiang b8ab38308d feature 参数校验支持国际化 2021-06-09 14:11:29 +08:00
wenjianzhang 91d902389a refactor🎨: 调整角色授权写法 2021-06-07 20:37:44 +08:00
wenjianzhang 5948c5c9a6 refactor🎨: 引用格式化 2021-06-07 20:37:20 +08:00
wenjianzhang 9a2c0729f6 refactor🎨: 缩减函数名称 2021-06-07 20:37:01 +08:00
wenjianzhang c96d9c35a7 refactor🎨 :支持名称和编码模糊搜索 2021-06-07 18:05:05 +08:00
zhangwenjian 65e0b58be9 refactor🎨: 去除log中间件日志打印 2021-06-07 16:41:21 +08:00
zhangwenjian dad4b4cca8 refactor🎨: 添加redis配置 2021-06-07 16:40:43 +08:00
zhangwenjian 3be02f1364 refactor🎨: 简化数据绑定 2021-06-07 16:39:59 +08:00
zhangwenjian 190dcfedc0 refactor🎨: 缩减函数名称 2021-06-07 16:39:26 +08:00
zhangwenjian 6e01c13e1c refactor🎨: 缩减函数名称 2021-06-07 16:38:30 +08:00
zhangwenjian bb77450f2c refactor🎨: 调整数据库模型字段类型,以适配其他数据库类型 2021-06-07 16:36:50 +08:00
wenjianzhang 4dfd4e4647 refactor🎨 :统一格式 2021-06-03 19:39:19 +08:00
zhangwenjian 9a49700d09 refactor🎨: 调整对象定义方式 2021-06-03 09:31:10 +08:00
zhangwenjian 107a8d8f90 docs📝: 修改GetPermissionFromContext函数注解 2021-06-03 09:30:04 +08:00
zhangwenjian b70b853016 refactor🎨: 调整格式 2021-06-03 09:29:19 +08:00
zhangwenjian dd2ed9f9a5 refactor🎨: login文档注解修改 2021-06-03 09:28:44 +08:00
zhangwenjian 1a645f8521 refactor🎨: 操作日志添加文档注解 2021-06-03 09:28:27 +08:00
zhangwenjian c05cb63bc7 refactor🎨: 改造升级用户管理模块 2021-06-03 09:27:34 +08:00
zhangwenjian 602ff26988 docs📝: 修改api文档注解 2021-06-03 09:23:48 +08:00
zhangwenjian b7d6b6786c docs📝: 添加参数api文档注解 2021-06-03 09:18:44 +08:00
zhangwenjian 95c794c80d docs📝: 修改菜单列表api文档注解 2021-06-03 09:18:11 +08:00
zhangwenjian e59dd88f97 docs📝: 更新api文档 2021-06-03 09:16:53 +08:00
zhangwenjian f31d1a137f perf👌 调整应用中间件 2021-05-31 23:52:56 +08:00
zhangwenjian b1d3924294 perf👌 优化job处理方式 2021-05-31 23:52:32 +08:00
zhangwenjian 526a993698 fix🐛 定时任务,触发函数,无法正常关闭(#432) 2021-05-31 23:51:47 +08:00
zhangwenjian f5d9a22f83 perf👌 调整菜单代码结构 2021-05-31 23:37:04 +08:00
zhangwenjian 5ae3ef3a15 perf👌 修正菜单搜索条件信息 2021-05-31 23:36:41 +08:00
zhangwenjian 0773de2fc5 perf👌 修正接口文档配置信息 2021-05-31 23:36:20 +08:00
zhangwenjian 48a57c4244 perf👌 更新版本 2021-05-31 23:35:03 +08:00
zhangwenjian 9b1cba311e perf👌 added menu type enum 2021-05-31 23:34:50 +08:00
zhangwenjian 06000a6dac perf👌 runtime接管 中间件 2021-05-31 18:11:19 +08:00
zhangwenjian a54d4ba0c1 format🥚 代码格式化 2021-05-31 18:10:23 +08:00
zhangwenjian a7cc943b8e Merge remote-tracking branch 'origin/dev' into dev 2021-05-31 18:04:59 +08:00
linwenxiang 0314991f9e fix 🐛 修复queue redis模式阿里云不工作问题 2021-05-31 14:39:27 +08:00
wenjianzhang 32d8f4384a feat 参数更新功能 2021-05-28 17:12:55 +08:00
wenjianzhang d69b7807c1 feat 添加修改配置接口 2021-05-27 19:33:41 +08:00
wenjianzhang dc32d92755 feat 开放部分中间件 2021-05-25 19:37:56 +08:00
wenjianzhang ca8fd2178d feat 操作日志多余参数去掉 2021-05-25 19:37:40 +08:00
wenjianzhang b565b72cd3 feat 上传文件去掉默认标识验证 2021-05-25 19:37:19 +08:00
wenjianzhang 8d272e8862 feat 排序字段接受参数名调整 2021-05-25 19:36:57 +08:00
wenjianzhang dd7b50e0fa feat 操作日志功能更换bind方式 2021-05-25 19:36:20 +08:00
wenjianzhang 623e796c92 feat 操作日志功能更换bind方式 2021-05-25 19:35:45 +08:00
wenjianzhang 126f62ce59 feat 参数设置添加接口文档 2021-05-25 19:35:05 +08:00
zhangwenjian 77c149af04 refactor🎨 api业务dto bind()、Generate()优化 2021-05-25 07:33:35 +08:00
wenjianzhang b21f665760 feat 添加队列和缓存的默认配置信息 2021-05-24 18:40:57 +08:00
wenjianzhang be36c595c3 feat 移除重复注册中间件 2021-05-24 18:40:32 +08:00
wenjianzhang 1ac6568f67 feat 日志中间件调整 2021-05-24 18:39:53 +08:00
wenjianzhang b902ab2a5f feat 用户模块添加列排序 2021-05-24 18:39:32 +08:00
wenjianzhang 5069c3074b feat 用户模块添加列排序 2021-05-24 18:39:21 +08:00
wenjianzhang e0bd26d837 feat 角色模块添加列排序 2021-05-24 18:39:00 +08:00
wenjianzhang cc74686108 feat 接口管理模块添加列排序 2021-05-24 16:30:21 +08:00
wenjianzhang 7880151ef2 refactor🎨 系统监控更名Monitor》ServerMonitor 2021-05-24 10:37:16 +08:00
wenjianzhang cfa88b5ecf refactor🎨 系统监控地址格式化 2021-05-24 10:35:52 +08:00
zhangwenjian 452a561309 feat 数据字典根据key获取 业务页面使用 2021-05-24 07:40:48 +08:00
zhangwenjian 2ac8f925c9 refactor🎨 api业务功能调整 2021-05-23 17:52:43 +08:00
zhangwenjian 6cbded6241 fix🐛 修复部门数据权限 2021-05-22 23:11:36 +08:00
zhangwenjian f40b300c16 docs📝 修改readme 2021-05-22 23:01:16 +08:00
zhangwenjian e473f158e8 refactor🎨 核心业务结构调整 2021-05-22 22:54:08 +08:00
wenjianzhang 408dcc5057 refactor🎨 部分功能重写 2021-05-21 18:25:51 +08:00
wenjianzhang 765775c54d Merge branch 'dev' of github.com:go-admin-team/go-admin into dev 2021-05-20 23:00:49 +08:00
wenjianzhang 44545f3d5b feat 结构统一调整 2021-05-20 22:59:09 +08:00
linwenxiang e23a1f615f feat 优化logger使用 2021-05-19 19:31:04 +08:00
linwenxiang d3fa3ecf6c Merge branch 'dev' of github.com:go-admin-team/go-admin into dev 2021-05-18 08:51:32 +08:00
wenjianzhang 6b1c3d9226 refactor🎨 api数据初始化调整 2021-05-17 23:09:35 +08:00
linwenxiang 70d4976595 Merge branch 'dev' of github.com:go-admin-team/go-admin into dev 2021-05-17 22:26:56 +08:00
wenjianzhang b2345039e4 refactor🎨 移除设置log函数 2021-05-16 21:47:49 +08:00
zhangwenjian 33275573f9 refactor🎨 链式调用改造 2021-05-16 16:17:37 +08:00
wenjianzhang 9fdde1a13b Merge pull request #424 from GizmoOAO/dev
fix🐛 修复vue模板的条件判断 #423
2021-05-15 17:55:13 +08:00
CunYu 09cc51e5de fix🐛 修复vue模板的条件判断 #423 2021-05-15 09:03:01 +08:00
wenjianzhang 20e7f79f4f refactor🎨 添加默认字段排序 2021-05-14 19:32:53 +08:00
wenjianzhang b82c3d9f99 refactor🎨 升级gin版本 2021-05-14 12:22:41 +08:00
wenjianzhang dfd460c50b feat apis路径简化 2021-05-14 12:19:22 +08:00
wenjianzhang 8adc79a5f7 feat 新增省市区基础数据 2021-05-13 08:45:27 +08:00
wenjianzhang 2580be08ab feat 优化写法 2021-05-12 18:50:13 +08:00
wenjianzhang fd9df69d1f feat 检查并写入api 2021-05-12 18:48:38 +08:00
wenjianzhang d0b2e8d03f feat 添加api管理 2021-05-12 18:48:01 +08:00
wenjianzhang 70998c424d refactor🎨 优化Context设置方法 2021-05-12 18:47:29 +08:00
wenjianzhang abcdf880fd refactor🎨 优化模版 2021-05-12 18:46:23 +08:00
linwenxiang fac18b28f0 Merge branch 'dev' of github.com:go-admin-team/go-admin into dev
 Conflicts:
	common/apis/api.go
2021-05-11 22:12:14 +08:00
wenjianzhang ee15329bd2 feat systables 数据迁移模块更新 2021-05-11 18:53:40 +08:00
wenjianzhang 98e0ee92a7 Default Changelist 2021-05-11 18:51:16 +08:00
wenjianzhang 54158b8824 feat systables 》 sys_tables 2021-05-11 18:50:40 +08:00
wenjianzhang 75cd2f374a feat gen模块文件名和路径规则调整 2021-05-11 18:49:41 +08:00
wenjianzhang a78cc33cd1 feat 重置router 2021-05-11 18:48:44 +08:00
wenjianzhang fea425c36c feat 分离gen router 2021-05-11 18:47:45 +08:00
wenjianzhang 61c89bca8d feat 重置config router 2021-05-11 18:43:52 +08:00
wenjianzhang 76e83de8cd feat 重置dept router 2021-05-11 18:42:56 +08:00
wenjianzhang c9d9350476 feat 分离字典相关路由 2021-05-11 18:42:20 +08:00
wenjianzhang 34a1eea45e feat 重置loginlog router 2021-05-11 18:41:43 +08:00
wenjianzhang c505884cf2 feat 修改删除接口传参方式 2021-05-11 18:40:59 +08:00
wenjianzhang b95f517a4c feat 重置operalog router 2021-05-11 18:39:48 +08:00
wenjianzhang 50da7825bb feat 重置post路由 2021-05-11 18:38:44 +08:00
wenjianzhang 0a3e3be12c feat 添加api管理 2021-05-11 18:35:55 +08:00
wenjianzhang c13d13ccf4 feat 添加api结构体 2021-05-11 18:34:57 +08:00
wenjianzhang 0317c3aaf5 feat 添加生成模块前端文件名 2021-05-11 18:33:29 +08:00
wenjianzhang 03eaad5b8b feat 统一api生成路径 2021-05-11 08:37:27 +08:00
wenjianzhang e091603bc8 feat 添加api管理功能 2021-05-10 18:24:47 +08:00
wenjianzhang cd29c1728d refactor🎨 删除模版生成提示代码 2021-05-10 18:23:41 +08:00
wenjianzhang 65b86353e3 fix🐛 修改合并代码产生的问题 2021-05-10 18:21:11 +08:00
wenjianzhang 5f594d6bae refactor🎨 修改参数删除,由url参数改为body 2021-05-10 18:20:14 +08:00
wenjianzhang bde8148eb1 Merge branch 'dev' of https://github.com/go-admin-team/go-admin into dev 2021-05-10 17:04:33 +08:00
wenjianzhang ae34ff8f82 feat优化router import 2021-05-10 17:02:26 +08:00
wenjianzhang ef33508e2b fix🐛 update 2021-05-08 18:29:14 +08:00
wenjianzhang a8c5f19075 refactor🎨 修改工具写法 2021-05-08 18:28:51 +08:00
wenjianzhang 1c046d9ddd Merge branch '1.3.x' into github-dev 2021-05-08 17:21:35 +08:00
wenjianzhang 6222765e20 feat优化配置信息 2021-05-08 16:50:51 +08:00
wenjianzhang f56839926e Merge remote-tracking branch 'github/dev' into github-dev 2021-05-08 13:53:49 +08:00
zhangwenjian c6a52134b3 feat优化字典数据错误判断写法 2021-05-08 13:53:27 +08:00
zhangwenjian 7fffcffcdd feat 分离app 2021-05-08 09:35:49 +08:00
linwenxiang b0225d9bab feat 增加bind通用方法 2021-04-27 11:48:03 +08:00
449 changed files with 46582 additions and 15612 deletions
+100
View File
@@ -0,0 +1,100 @@
---
name: new-business-module
description: Scaffold a new single-table CRUD business module end to end — migration, Actions-mode model/dto/router, and the sys_menu/sys_api/casbin seed data that makes it show up in the UI with working permissions. Use when the user wants to add a new business table/module to go-admin, not for cross-table or non-CRUD business logic.
---
# 新增业务模块
给一张新的业务表配齐"能跑、能看见、能授权"的完整闭环:迁移 → 后端代码 → 菜单与权限种子数据。
只适用于单表增删改查;跨表事务、外部调用、复杂校验等超出这个范围(见下方"何时不适用")。
开始前先读 `AGENTS.md`(分层边界、通用 Action 使用前提、命名规则)和 `app/demo/` 下的全部文件——
这是可编译、有测试、CI 会跑的参照物,本文与它冲突时以它为准。
## 何时不适用
业务超出单表 CRUD(跨表事务、外部服务调用、复杂校验)时,不要用这个 skill 硬套——
改成手写 Handler + Service,参照 `app/admin/apis/sys_post.go` 及其 Service,遵守
`AGENTS.md` 的分层约束(Api 不碰 OrmService 不碰 `gin.Context`,一律用 `e.Orm`)。
## 步骤
### 1. 确认表结构
表结构需符合命名规范:`sys_`/业务前缀 + 下划线(如 `tb_article`)。核对字段是否已有
`created_at`/`updated_at`/`deleted_at` 这类约定字段。
### 2. 写数据库迁移
放在 `cmd/migrate/migration/version/` 目录(**不是** `version-local/` —— 后者在
`.gitignore` 中,提交时会被忽略,`git status` 也看不到)。
- 文件名前 13 位是时间戳版本号
- 已执行过的迁移文件不可修改;需要修正时新增一个迁移
- 包名为 `version`
### 3. 生成 model / dto / router 三个文件(Actions 模式)
不要手写 Api 与 Service。使用 `common/actions` 的通用 Action,一个模块只需
model、dto、router 三个文件,完整写法照抄 `app/demo/` 的结构。
**关键正确性要求**(这三条是实际出问题最多的地方):
- Model 实现 `models.ActiveRecord``Generate` / `GetId` / `TableName`),
`TableName()` 必须显式声明——GORM 配置了 `SingularTable`,不会自动推导
- **`Generate()` 必须返回副本,不要就地返回**——Action 在并发请求间复用实例,
就地返回会导致请求之间串数据;这个问题单人测试时几乎不出现,上线后才暴露
- 完成后确认 `cmd/api/` 中已用 `_` 导入新包,否则路由不会被注册
### 4. 写菜单、接口与权限种子数据
这一步最容易被漏掉——代码能编译、接口能测通,但界面上看不到菜单、点了按钮说
没权限,往往就是漏了这一步。结构参照 `cmd/migrate/migration/version/1786700001000_demo_menu.go`
——它是可运行、幂等(用 `upsert`,重复跑不会报错)的真实例子。
:::danger
**但不要照抄它的 import。** 那个文件用的是 `cmd/migrate/migration/models`
只因为它的版本号排在软删除转换(`1786700003000`)之前才是安全的。
**你新写的迁移版本号在转换之后,必须改用 `app/` 下的运行时模型**
`app/admin/models.SysApi``SysMenu`),否则第一条 insert 就会
`NOT NULL constraint failed: sys_api.deleted_at`
`TestPostConversionMigrationsAvoidFrozenSeedModels` 会拦住这个错误。
:::
一个模块要在界面上可用,需要四类数据,缺一样都不行:
| 表 | 作用 |
|---|---|
| `sys_api` | 后端路由登记,Casbin 据此判定权限 |
| `sys_menu` | 侧边栏菜单(目录用 `M`、菜单用 `C`、按钮用 `F` |
| `sys_menu_api_rule` | 菜单与接口的多对多关联,角色保存时据此生成策略 |
| `casbin_rule` | 实际生效的权限策略(**不是** `sys_casbin_rule`,那张表的唯一索引在 MySQL 下会超长,不要迁移它) |
必须核对的两处一致性——**错了不会报错,只会在界面上表现为"看不到/点不动"**
- `sys_menu.menu_name` 必须与前端组件的 `defineOptions({ name: 'XxxManage' })` 一致,
否则 `keep-alive` 缓存静默失效
- 按钮级 `sys_menu.permission`(格式 `模块:资源:操作`)必须与前端
`v-permisaction="['模块:资源:操作']"` 完全一致,否则按钮权限判断静默失效
### 5. 收尾检查
| 检查项 | 出错后果 |
| --- | --- |
| `Generate()` 是否返回副本 | 并发请求之间串数据 |
| 是否使用 `e.Orm` 而非全局 DB | 多租户下拿到错误的数据库连接 |
| `TableName()` 是否显式声明 | GORM 不会自动推导 |
| 迁移文件是否放在 `version/` | 放进 `version-local/` 会被忽略,别人拉代码看不到 |
| `sys_menu.menu_name` 是否与前端组件 `name` 一致 | keep-alive 缓存静默失效 |
| `sys_menu.permission` 是否与前端 `v-permisaction` 一致 | 按钮权限静默失效 |
跑一遍 `go run -tags sqlite3 . migrate -c config/settings.sqlite.yml` 验证迁移可执行,
`go run -tags sqlite3 . server -c config/settings.sqlite.yml` 启动服务,用 admin
账号登录确认新菜单和按钮权限都出现了。
如果前端页面还没生成,下一步用 go-admin-ui 仓库里的 `new-list-page` skill——两边靠
`sys_menu.permission` / `v-permisaction` 这个字符串对齐。
> 不要把 `config/settings.yml` 的真实内容贴给 AI 工具——`database.source` 含数据库
> 账号密码,`jwt.secret` 泄露后可被用来伪造任意用户的 token。
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: 🆕 Create new issue
url: http://new-issue.go-admin.dev
about: The issue which is not created via http://new-issue.go-admin.dev will be closed immediately.
- name: 🆕 创建一个新 Issue
url: http://new-issue.go-admin.dev
about: 不是用 http://new-issue.go-admin.dev 创建的 issue 会被机器人自动关闭。
+66
View File
@@ -0,0 +1,66 @@
<!--
First of all, thank you for your contribution! 😄
For requesting to pull a new feature or bugfix, please send it from a feature/bugfix branch based on the `master` branch.
Before submitting your pull request, please make sure the checklist below is confirmed.
Your pull requests will be merged after one of the collaborators approve.
Thank you!
-->
[[中文版模板 / Chinese template](https://github.com/go-admin-team/go-admin/blob/master/.github/PULL_REQUEST_TEMPLATE/pr_cn.md)]
### 🤔 This is a ...
- [ ] New feature
- [ ] Bug fix
- [ ] Site / documentation update
- [ ] Demo update
- [ ] Component style update
- [ ] TypeScript definition update
- [ ] Bundle size optimization
- [ ] Performance optimization
- [ ] Enhancement feature
- [ ] Internationalization
- [ ] Refactoring
- [ ] Code style optimization
- [ ] Test Case
- [ ] Branch merge
- [ ] Other (about what?)
### 🔗 Related issue link
<!--
1. Put the related issue or discussion links here.
-->
### 💡 Background and solution
<!--
1. Describe the problem and the scenario.
2. GIF or snapshot should be provided if includes UI/interactive modification.
3. How to fix the problem, and list the final API implementation and usage sample if that is a new feature.
-->
### 📝 Changelog
<!--
Describe changes from the user side, and list all potential break changes or other risks.
--->
| Language | Changelog |
| ---------- | --------- |
| 🇺🇸 English | |
| 🇨🇳 Chinese | |
### ☑️ Self-Check before Merge
⚠️ Please check all items below before review. ⚠️
- [ ] Doc is updated/provided or not needed
- [ ] Demo is updated/provided or not needed
- [ ] TypeScript's definition is updated/provided or not needed
- [ ] Changelog is provided or not needed
+61
View File
@@ -0,0 +1,61 @@
<!--
首先,感谢你的贡献!😄
新特性请提交至 feature 分支,其余可提交至 master 分支。
在维护者审核通过后会合并。
请确保填写以下 pull request 的信息,谢谢!~
-->
[[English Template / 英文模板](https://github.com/go-admin-team/go-admin/blob/master/.github/PULL_REQUEST_TEMPLATE.md)]
### 🤔 这个变动的性质是?
- [ ] 新特性提交
- [ ] 日常 bug 修复
- [ ] 站点、文档改进
- [ ] 演示代码改进
- [ ] 组件样式/交互改进
- [ ] TypeScript 定义更新
- [ ] 包体积优化
- [ ] 性能优化
- [ ] 功能增强
- [ ] 国际化改进
- [ ] 重构
- [ ] 代码风格优化
- [ ] 测试用例
- [ ] 分支合并
- [ ] 其他改动(是关于什么的改动?)
### 🔗 相关 Issue
<!--
1. 描述相关需求的来源,如相关的 issue 讨论链接。
-->
### 💡 需求背景和解决方案
<!--
1. 要解决的具体问题。
2. 列出最终的 API 实现和用法。
3. 涉及UI/交互变动需要有截图或 GIF。
-->
### 📝 更新日志
<!--
从用户角度描述具体变化,以及可能的 breaking change 和其他风险。
-->
| 语言 | 更新描述 |
| ------- | -------- |
| 🇺🇸 英文 | |
| 🇨🇳 中文 | |
### ☑️ 请求合并前的自查清单
⚠️ 请自检并全部**勾选全部选项**。⚠️
- [ ] 文档已补充或无须补充
- [ ] 代码演示已提供或无须提供
- [ ] TypeScript 定义已补充或无须补充
- [ ] Changelog 已提供或无须提供
+206
View File
@@ -0,0 +1,206 @@
name: Build
# Documentation-only changes, and changes confined to the Kubernetes
# manifests, skip this workflow entirely.
#
# A push to master here does not just build - it pushes an image, runs the
# migrations and restarts the demo container, so the site takes a short outage.
# Paying that for a README edit is waste at best; at worst a deploy fails for a
# reason unrelated to anything in the change. Code coverage is unaffected,
# because go.yml still builds every push and pull request.
#
# scripts/k8s holds deploy.yml, storage.yml and prerun.sh, and the deploy below
# reads none of them - it is an ssh into one host that runs docker, building the
# Dockerfile at the repository root. Those manifests are for people deploying to
# a cluster of their own. The pattern is scripts/k8s/** rather than scripts/**
# because scripts/Dockerfile is a build input: go.yml builds the release image
# from it on a tag.
#
# A file outside these patterns still runs the workflow even when the rest of
# the change is ignorable: paths-ignore skips only when every changed path
# matches. Editing this file is one such case, on purpose - a deploy script
# that is never exercised by the change that broke it is worse than an outage.
on:
push:
branches: [ master ]
paths-ignore:
- '**.md'
- 'docs/**'
- 'LICENSE*'
- '.github/ISSUE_TEMPLATE/**'
- 'scripts/k8s/**'
pull_request:
branches: [ master ]
paths-ignore:
- '**.md'
- 'docs/**'
- 'LICENSE*'
- '.github/ISSUE_TEMPLATE/**'
- 'scripts/k8s/**'
# One deploy at a time. Two merges seconds apart raced here: both runs did
# docker rm -f then docker run, the second removed the container the first had
# just created, and the first's docker run then failed on a name conflict -
# leaving the demo on the older image with a red deploy.
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false
env:
IMAGE_NAME: registry.ap-northeast-1.aliyuncs.com/go-admin/go-admin-api # 镜像名称
TAG: ${{ github.sha }}
IMAGE_NAME_TAG: registry.ap-northeast-1.aliyuncs.com/go-admin/go-admin-api:${{ github.sha }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: 1.26.5
- name: Tidy
run: go mod tidy
- name: Build
run: env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -tags "sqlite3,json1" --ldflags "-extldflags -static" -o main .
# 以下推镜像与重启步骤仅在 master 收到 push 时执行。
# pull_request 事件同样会触发本工作流,若不加限制,任何指向 master 的
# PR 一经创建就会把 PR 分支的镜像推上仓库,并直接重启线上 API 服务,
# 且发生在合并之前。构建与编译校验不受影响,PR 仍会执行。
- name: Build the Docker image and push
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
run: |
docker login --username=${{ secrets.DOCKER_USERNAME }} registry.ap-northeast-1.aliyuncs.com --password=${{ secrets.DOCKER_PASSWORD }}
echo "************ docker login end"
docker build -t go-admin-api:latest .
echo "************ docker build end"
docker tag go-admin-api ${{ env.IMAGE_NAME_TAG }}
echo "************ docker tag end"
docker images
echo "************ docker images end"
docker push ${{ env.IMAGE_NAME_TAG }} # 推送
echo "************ docker push end"
- name: Restart server # 第五步,重启服务
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
env:
GITHUB_SHA_X: ${GITHUB_SHA}
with:
host: ${{ secrets.SSH_HOST }} # 下面三个配置与上一步类似
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.DEPLOY_KEY }}
# 重启的脚本,根据自身情况做相应改动,一般要做的是migrate数据库以及重启服务器
#
# 配置从宿主机挂载,不使用镜像里的那份:演示站连的是托管数据库,
# 而 config/settings.demo.yml 会随仓库公开、也会打进镜像,凭据不能写在那里。
# 镜像里那份保持 sqlite,供 clone 仓库的人开箱即用。
#
# 路径本身走 secret:它不是凭据,但本仓库公开,没有理由把服务器的
# 目录结构一并公布。DEMO_CONFIG_PATH 指向宿主机上那份配置。
#
# 顺序是有意的:迁移先跑,跑不过就保持现有版本不动;
# 旧容器改名保留而不是删除,新容器不健康时能原样恢复。
# 健康检查两条都要过——HTTP 活着不代表数据库通了。
script: |
set -u
CFG="${{ secrets.DEMO_CONFIG_PATH }}"
IMG="${{ env.IMAGE_NAME_TAG }}"
NAME=go-admin-api
PREV="$NAME-prev"
test -f "$CFG" || { echo "宿主机配置缺失,中止部署"; exit 1; }
# Old images of this repository are removed here and nowhere else.
# Every deployment pulls one tagged with its commit and nothing ever
# removed the previous one, so they only accumulated: 68 of them
# filled the disk and the next deployment could not pull.
#
# Three are kept so a release can be re-run by tag by hand.
#
# Only this repository's images are listed, because the host runs
# other services whose images are not this script's business. The
# image the live container is on is excluded by id rather than by
# position, so it survives even if the listing order is not what
# it looks like. With no container to ask, the function returns
# rather than running the pipeline on an empty id - which would
# also delete nothing, but by way of grep -v matching every line,
# which reads like the opposite of what it does. No -f, so an image
# any container still holds - including the one kept for rollback -
# is refused rather than taken away from it.
prune_old_images() {
REPO="${IMG%:*}"
LIVE=$(sudo docker inspect -f '{{.Image}}' "$NAME" 2>/dev/null | sed 's/^sha256://' | cut -c1-12)
[ -n "$LIVE" ] || return 0
sudo docker images "$REPO" --format '{{.ID}} {{.Repository}}:{{.Tag}}' \
| grep -v "^$LIVE" \
| tail -n +3 \
| awk '{print $2}' \
| xargs -r -n1 sudo docker rmi >/dev/null 2>&1 || true
}
sudo docker login --username=${{ secrets.DOCKER_USERNAME }} registry.ap-northeast-1.aliyuncs.com --password=${{ secrets.DOCKER_PASSWORD }}
# Before the pull, not only after a successful deploy. The pull
# is the first thing here that needs space and it is where a full
# disk stops this script, so a cleanup that only runs afterwards
# never runs on the host that needs it: rerunning the workflow
# fails at the same pull, and the disk has to be cleared by hand.
prune_old_images
sudo docker pull "$IMG" || { echo "拉取镜像失败,中止部署"; exit 1; }
# 迁移用新镜像跑。失败时线上仍是旧版本配旧 schema,是自洽的;
# 硬切过去才会得到代码与表对不上的服务。
if ! sudo docker run --rm -v "$CFG":/config/settings.yml:ro "$IMG" \
/main migrate -c /config/settings.yml; then
echo "迁移失败,保持现有版本"; exit 1
fi
if sudo docker ps -a --format '{{.Names}}' | grep -qx "$NAME"; then
sudo docker rm -f "$PREV" >/dev/null 2>&1 || true
sudo docker rename "$NAME" "$PREV"
# --timeout, because the default is 10 seconds and the process
# spends drain + server + cleanup from extend.shutdown before it
# exits - 8 seconds out of the box, and more for anyone who
# configures a drain window. Past the deadline docker sends
# SIGKILL and the cleanup callbacks are cut off part-way through.
# checksilent's docker-stop-cuts-shutdown-short check compares
# this number against config/settings.yml.
sudo docker stop --timeout 30 "$PREV" >/dev/null
fi
sudo docker run -d -p 8000:8000 \
-v "$CFG":/config/settings.yml:ro \
--name "$NAME" "$IMG"
ok=0
for i in $(seq 1 20); do
sleep 3
code=$(curl -s -o /dev/null -w '%{http_code}' -m 5 http://127.0.0.1:8000/api/v1/captcha 2>/dev/null || true)
if [ "$code" = "200" ] && sudo docker logs "$NAME" 2>&1 | grep -q 'connect success'; then
ok=1; echo "健康检查通过(第 $i 次探测)"; break
fi
done
if [ "$ok" = "1" ]; then
sudo docker rm -f "$PREV" >/dev/null 2>&1 || true
# Again, so the image this deployment replaced falls out of the
# window rather than waiting for the next deployment to notice.
prune_old_images
else
echo "健康检查失败,回滚到上一版本"
sudo docker logs --tail 40 "$NAME" 2>&1 || true
sudo docker rm -f "$NAME" >/dev/null 2>&1 || true
if sudo docker ps -a --format '{{.Names}}' | grep -qx "$PREV"; then
sudo docker rename "$PREV" "$NAME"
sudo docker start "$NAME" >/dev/null
echo "已恢复"
fi
exit 1
fi
+4 -4
View File
@@ -19,11 +19,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@24c7eb380a2dc368f2d129e4c65e51d172983a1e # v4
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -34,7 +34,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@24c7eb380a2dc368f2d129e4c65e51d172983a1e # v4
# ️ Command-line programs to run using the OS shell. # ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -48,4 +48,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@24c7eb380a2dc368f2d129e4c65e51d172983a1e # v4
-60
View File
@@ -1,60 +0,0 @@
name: build
on:
push:
branches: [ dev-lwx ]
pull_request:
branches: [ dev-lwx ]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.15
uses: actions/setup-go@v1
with:
go-version: 1.15
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: |
go get -v -t -d ./...
if [ -f Gopkg.toml ]; then
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
dep ensure
fi
- name: Build
run: |
CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -a -installsuffix cgo -o go-admin .
mv go-admin ./scripts
- uses: Azure/docker-login@v1
with:
login-server: registry.cn-shanghai.aliyuncs.com
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- run: |
docker build ./scripts -t registry.cn-shanghai.aliyuncs.com/go-admin-team/go-admin:${{ github.sha }}
docker push registry.cn-shanghai.aliyuncs.com/go-admin-team/go-admin:${{ github.sha }}
- uses: Azure/k8s-set-context@v1
with:
kubeconfig: ${{ secrets.KUBE_CONFIG }}
- uses: Azure/k8s-create-secret@v1
with:
namespace: 'go-admin'
container-registry-url: registry.cn-shanghai.aliyuncs.com
container-registry-username: ${{ secrets.REGISTRY_USERNAME }}
container-registry-password: ${{ secrets.REGISTRY_PASSWORD }}
secret-name: aliyuncs-k8s-secret
- uses: Azure/k8s-deploy@v1
with:
namespace: 'go-admin'
manifests: 'scripts/k8s/deploy.yml'
images: 'registry.cn-shanghai.aliyuncs.com/go-admin-team/go-admin:${{ github.sha }}'
imagepullsecrets: 'aliyuncs-k8s-secret'
kubectl-version: 'latest'
+172 -12
View File
@@ -2,33 +2,193 @@ name: build
on: on:
push: push:
branches: [ master ] branches: [ master, dev ]
tags: [ 'v*', '[0-9]*' ]
pull_request: pull_request:
branches: [ master ] branches: [ master ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs: jobs:
build: build:
name: Build name: Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
# The queue's ordering rule - consumers registered before the queue is
# started - is invisible on the memory backend, which is the default and
# therefore what every other test runs on: queue.Memory's Register starts a
# consumer goroutine whatever the state. Only redis refuses a late
# registration, so without a server here the tests that cover it would skip
# and the suite would report success for a queue that accepts no consumers.
services:
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 3s
--health-retries 10
postgres:
image: postgres:15-alpine
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: goadmin_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 3s
--health-retries 10
# The fourth registered driver, and the one that disagrees with the
# other three about NULL: its unique index treats two NULLs as equal and
# permits one. A migration that builds a unique index over a nullable
# column therefore fails here and nowhere else, which is how one shipped
# that no SQL Server database could apply at all - not even an empty
# one. The password is this container's only credential and the
# container lives for the length of one job.
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
env:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: GoAdmin_Test1
MSSQL_PID: Developer
ports:
- 1433:1433
options: >-
--health-cmd "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P GoAdmin_Test1 -C -Q 'SELECT 1'"
--health-interval 10s
--health-timeout 5s
--health-retries 20
--health-start-period 20s
# The dialect most installations actually run, and until the
# scheduler lease (#915) the only one with no service here. The lease
# reads the database's clock, and the first implementation read it as
# a timestamp: over go-admin's own `parseTime=True&loc=Local` DSN,
# MySQL's UTC_TIMESTAMP comes back relabelled as local time, so on any
# host that is not UTC every lease was one zone offset out - and every
# assertion that compared the lease only against itself still passed.
# The three dialects that were here could not see it.
mysql:
image: mysql:8
env:
MYSQL_ROOT_PASSWORD: GoAdmin_Test1
MYSQL_DATABASE: goadmin_test
ports:
- 3306:3306
options: >-
--health-cmd "mysqladmin ping -h 127.0.0.1 -uroot -pGoAdmin_Test1"
--health-interval 10s
--health-timeout 5s
--health-retries 20
--health-start-period 20s
env:
GO_ADMIN_TEST_REDIS_ADDR: 127.0.0.1:6379
# The soft-delete conversion drops an index, and gorm's PostgreSQL driver
# produced unparseable SQL for that - on SQLite, where the rest of these
# tests run, the same code works. The suite reported success for a
# migration that failed on every PostgreSQL database it was pointed at.
# See go-admin#919.
GO_ADMIN_TEST_POSTGRES_DSN: "host=127.0.0.1 port=5432 user=postgres password=postgres dbname=goadmin_test sslmode=disable"
GO_ADMIN_TEST_SQLSERVER_DSN: "sqlserver://sa:GoAdmin_Test1@127.0.0.1:1433?database=goadmin_test"
# loc=Local on purpose: it is what config/settings.yml ships and what
# made the timezone defect above reachable. A DSN here that quietly
# differed from the one installations use would test a configuration
# nobody runs.
GO_ADMIN_TEST_MYSQL_DSN: "root:GoAdmin_Test1@tcp(127.0.0.1:3306)/goadmin_test?charset=utf8mb4&parseTime=True&loc=Local"
steps: steps:
- name: Set up Go 1.14 - name: Set up Go 1.26
uses: actions/setup-go@v1 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with: with:
go-version: 1.14 go-version: 1.26.5
id: go id: go
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# SQL Server has no equivalent of POSTGRES_DB, so the database the DSN
# names has to be created before the tests run.
- name: Create the SQL Server test database
run: |
docker exec ${{ job.services.sqlserver.id }} /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P GoAdmin_Test1 -C \
-Q "IF DB_ID('goadmin_test') IS NULL CREATE DATABASE goadmin_test"
- name: Get dependencies - name: Get dependencies
run: | run: go mod tidy
go get -v -t -d ./...
if [ -f Gopkg.toml ]; then # Before the tests rather than beside checksilent at the end: a formatting
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh # miss is a one-command fix, and finding out about it after five minutes of
dep ensure # tests and an end-to-end install is five minutes nobody gets back.
fi - name: Formatting
run: make fmt-check
# go build does not compile _test.go, so building alone never ran a single
# test. This is the only workflow that fires on every push and pull request,
# which makes it the one place a test gate belongs.
- name: Test
run: make test
- name: Build - name: Build
run: go build -v . run: make build
# A separate module, so none of the steps above see it: the main module's
# go.mod, its build and its tests are all unaware of the example
# application. This is the only thing that exercises an application being
# installed at all - everything below it runs against an injected engine
# and a hand-built schema, and none of that can catch an application's
# init() reaching one registry and not the other.
- name: End-to-end install and uninstall
run: make test-e2e
# Fails the build on the silent-failure classes listed in
# tools/checksilent, one of which is the contract boundary: nothing under
# common/ may import app/. A boundary that is only written down erodes; this
# is what keeps it true.
- name: Silent-failure checks
run: make checksilent
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
if: startsWith(github.ref, 'refs/tags/')
- name: Log in to the Container registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
if: startsWith(github.ref, 'refs/tags/')
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
if: startsWith(github.ref, 'refs/tags/')
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=auto
tags: |
type=schedule
type=ref,event=tag
type=sha,prefix=,format=long,enable=true,priority=100
- name: Build and push Docker image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
if: startsWith(github.ref, 'refs/tags/')
with:
context: .
file: scripts/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+24 -6
View File
@@ -1,15 +1,19 @@
.idea .idea
.vscode .vscode
*/.DS_Store .DS_Store
static/uploadfile static/uploadfile
main.exe main.exe
*.exe
go-admin go-admin
go-admin.exe
# `go build ./tools/checksilent` drops the binary here, next to the one for the
# server. Anchored with a leading slash: unanchored, the same pattern matches
# tools/checksilent/ as well and the tool's own source never gets committed.
/checksilent
temp/ temp/
!temp !temp
vendor vendor
config/settings.dev.yml config/settings.dev.yml
go-admin
common/middleware/demo.go
config/settings.dev.*.yml config/settings.dev.*.yml
config/settings.dev.*.yml.log config/settings.dev.*.yml.log
temp/logs temp/logs
@@ -17,8 +21,22 @@ config/settings.dev.yml.log
config/settings.b.dev.yml config/settings.b.dev.yml
cmd/migrate/migration/version-local/* cmd/migrate/migration/version-local/*
!cmd/migrate/migration/version-local/doc.go !cmd/migrate/migration/version-local/doc.go
*/.DS_Store
# go sum
go.sum
config/settings.deva.yml config/settings.deva.yml
go-admin-server
CLAUDE.md
# Everything under .claude is private by default. Skills meant for people using
# go-admin are re-included one directory at a time, so a personal one dropped in
# here is never committed by accident.
.claude/*
!.claude/skills/
.claude/skills/*
!.claude/skills/new-business-module/
config/settings.local.dev.yml
# Go workspace files. They exist to point this module at a local checkout of
# go-admin-core while the two are developed together, which is a private
# arrangement between one machine's directories - committing one would break
# the build for everyone else.
go.work
go.work.sum
+273
View File
@@ -0,0 +1,273 @@
# AGENTS.md — go-admin 后端
> 给 AI 编码工具与新贡献者的约定。**只写"不遵守就会出错"的规则**;技术栈版本以
> `go.mod` 为准,命令以 `Makefile` 为准,此处不复述,避免与代码脱节。
>
> 标准 CRUD 模块的完整写法见 **`app/demo/`** —— 那是可编译、有测试、CI 会跑的参照物。
> 本文与它冲突时,以 `app/demo/` 为准。
## 分层
```
Router → Api → Service → Model
路由注册 参数绑定 业务逻辑 GORM 结构体
中间件链 调用 Service 操作数据库 TableName()
```
对应目录:`app/{模块}/router|apis|service|models`DTO 位于 `service/dto`
**不可跨层**Api 不直接操作 `Orm`Service 不接触 `gin.Context`
## 优先使用通用 Action
单表 CRUD **不要手写 Api 与 Service**`common/actions` 提供的五个
Action 已覆盖参数绑定、数据权限过滤、操作人注入、分页与错误响应:
```go
r := v1.Group("/demo-product").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
{
m := &models.DemoProduct{}
r.GET("", actions.PermissionAction(), actions.IndexAction(m, new(dto.DemoProductSearch), func() interface{} {
list := make([]models.DemoProduct, 0); return &list
}))
r.GET("/:id", actions.PermissionAction(), actions.ViewAction(new(dto.DemoProductById), func() interface{} {
return &models.DemoProduct{}
}))
r.POST("", actions.CreateAction(new(dto.DemoProductControl)))
r.PUT("/:id", actions.PermissionAction(), actions.UpdateAction(new(dto.DemoProductControl)))
r.DELETE("", actions.PermissionAction(), actions.DeleteAction(new(dto.DemoProductById)))
}
```
这样一个模块只需 **model + dto + router** 三个文件,完整示例见 `app/demo/`
使用通用 Action 的前提:
- Model 实现 `models.ActiveRecord``Generate` / `GetId` / `TableName`
- 列表 DTO 实现 `dto.Index`,增改删 DTO 实现 `dto.Control`
- **所有 `Generate()` 必须返回副本** —— Action 在并发请求间复用实例,
就地返回会串数据(`app/demo` 的测试锁定了这一点)
- 详情/删除 DTO 内嵌 `dto.ObjectById` 即可继承 `Bind``GetId`,无需重写
仅当业务超出单表 CRUD(跨表事务、外部调用、复杂校验)时才自行编写 Api
与 Service,写法见下。
## Api 层(仅在通用 Action 不适用时)
结构体嵌入 `api.Api`,链式初始化后**必须检查 `Errors`**
```go
func (e SysPost) GetPage(c *gin.Context) {
s := service.SysPost{}
req := dto.SysPostPageReq{}
err := e.MakeContext(c).MakeOrm().Bind(&req, binding.Form).MakeService(&s.Service).Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
// ... 调用 s.GetPage(...)
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}
```
响应一律走 `e.OK` / `e.PageOK` / `e.Error`,不要自行 `c.JSON`
## Service 层(仅在通用 Action 不适用时)
结构体嵌入 `service.Service`(持有 `Orm``Log`)。查询通过 Scopes 组合:
```go
err = e.Orm.Model(&data).Scopes(
cDto.MakeCondition(c.GetNeedSearch()), // 由 search tag 生成 WHERE
cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
actions.Permission(data.TableName(), p), // 数据权限,列表/详情必须带
).Find(list).Limit(-1).Offset(-1).Count(count).Error
```
**遗漏 `actions.Permission` 会使数据权限配置静默失效** —— 这是最容易出的错。
错误一律 `return err` 向上传递,日志用 `e.Log.Errorf`,不使用 `panic`
## DTO
搜索条件由 tag 声明,`MakeCondition` 据此拼 SQL
```go
type SysPostPageReq struct {
dto.Pagination `search:"-"`
PostName string `form:"postName" search:"type:contains;column:post_name;table:sys_post"`
}
func (m *SysPostPageReq) GetNeedSearch() interface{} { return *m }
```
`type` 可选:`exact` `iexact` `contains` `gt` `gte` `lt` `lte` `order` `left`(联表)。
## Model
```go
type SysPost struct {
PostId int `gorm:"primaryKey;autoIncrement" json:"postId"`
// ... 业务字段
models.ControlBy // CreateBy / UpdateBy
models.ModelTime // CreatedAt / UpdatedAt / DeletedAt
}
func (SysPost) TableName() string { return "sys_post" }
```
`TableName()` 必须显式声明(GORM 配置了 `SingularTable`,不会自动推导复数)。
## 公共契约面
第三方应用(`app/` 下的业务模块)可以稳定依赖哪些包、路由与迁移怎么注册、
哪些约束是硬的,见 `docs/contract.md`
两条与主仓贡献者直接相关的:
- **`common/``core/` 不得 import `app/`** —— `make checksilent` 在 CI 里守着,违反即红。
- **从 core 契约包声明出来的类型必须写成别名**(`type X = pkg.Y`,不是 `type X pkg.Y`
—— `contract-shim-alias` 检查守着。defined type 会丢掉整个方法集,
而且**不一定在本仓编译失败**,理由见 `docs/contract.md` 末节。
- **注册类 API`AppRouters` / `sdk.Runtime.SetAppRouters` / `migration.ForApp`
必须在 `runStartupHooks()` 之前调用完** —— `init()` 是最省事的位置,
但约束的是**顺序**,不是写在哪个函数里;晚到的注册会被丢弃并只记一条 ERROR。
## 路由注册
通过 `init()` 自注册,不在中心文件手工添加:
```go
func init() { routerCheckRole = append(routerCheckRole, registerSysPostRouter) }
func registerSysPostRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := apis.SysPost{}
r := v1.Group("/post").
Use(authMiddleware.MiddlewareFunc()).
Use(middleware.AuthCheckRole()). // Casbin 鉴权
Use(actions.PermissionAction()) // 注入数据权限
{ r.GET("", api.GetPage); r.POST("", api.Insert); /* ... */ }
}
```
新增路由文件后,需确认 `cmd/api/` 中已用 `_` 导入该包。
## 命名
| 对象 | 规则 | 示例 |
|---|---|---|
| 数据表 | `sys_` 前缀 + 下划线 | `sys_post` |
| API 路径 | `/api/v1/` + kebab-case | `/api/v1/sys-user` |
| DTO | `{Model}{Action}Req` | `SysPostPageReq` |
| 权限标识 | `模块:资源:操作` | `admin:sysPost:add` |
权限标识需与前端 `v-permisaction` 一致,并写入 `sys_menu` 种子数据——完整可运行的
参照见 `cmd/migrate/migration/version/1786700001000_demo_menu.go`sys_api /
sys_menu / sys_menu_api_rule / casbin_rule 四张表如何配齐,用的是幂等 upsert,
可以直接照抄结构)。
## Swagger
Api 必须带完整注解,`go generate` 会据此生成文档:
```go
// @Summary 岗位列表
// @Tags 岗位
// @Success 200 {object} response.Response
// @Router /api/v1/post [get]
// @Security Bearer
```
## 本地运行
**配置 `driver: sqlite3` 时必须带构建标签**,否则启动即 panic
```bash
go run -tags sqlite3 . migrate -c config/settings.sqlite.yml
go run -tags sqlite3 . server -c config/settings.sqlite.yml
```
原因:`common/database/open.go``//go:build !sqlite3`,不加标签时编进的是
不含 sqlite3 的版本,`opens["sqlite3"]` 为 nil,调用时在 nil 函数上崩溃。
报错信息不会提到构建标签,容易误判成环境损坏。MySQL / PostgreSQL 无此问题。
对应 `Makefile``build-sqlite` 目标。
## 数据库迁移
文件名前 13 位为毫秒时间戳版本号,不合规的名字会在启动时 panic 并报出该文件名。
**已执行过的迁移文件不可修改** ——
`sys_migration` 表按版本号去重,改动不会重跑,只能新增一个迁移来修正。
放哪个目录取决于身份:
| 目录 | 用途 | 是否入库 |
|---|---|---|
| `version/` | 框架自带迁移,随仓库分发给所有使用者 | 是 |
| `version-local/` | 使用者自己项目的迁移 | 否(已在 `.gitignore` |
**向本仓库提交迁移必须放 `version/`** —— 放进 `version-local/` 会被忽略掉,
`git status` 看不到,PR 里也不会出现。两个目录的包名分别是 `version`
`version_local`(后者与目录名不一致,因为标识符不能含连字符)。
### 写种子数据用哪个 models 包
`1786700003000` 之后新增的迁移,**种子数据要用 `app/` 下的运行时模型**
(如 `app/admin/models.SysApi``SysMenu`),**不要用 `cmd/migrate/migration/models`**。
后者的 `ModelTime` 声明的是可空的 `gorm.DeletedAt`,这对它之前的迁移是对的(那正是
当时列的形状),转换之后就不再成立,两个方向都会出问题:
- **写**:往 NOT NULL 列里塞 NULL,第一条 insert 就 `NOT NULL constraint failed`
- **读**GORM 拼 `WHERE deleted_at IS NULL`,而活跃行存的是 `0`,静默查不到——
照抄 `demo_menu.go` 的授权段落会因此跳过授权,菜单建好、权限没授、迁移仍记为成功
干净库跑不出这个问题,今天所有用该包的迁移都排在转换之前。完整推导见
`schema_coverage_test.go``TestPostConversionMigrationsAvoidFrozenSeedModels`
的注释,那个测试也守着这条边界。
## 静默失败校验
`make checksilent` 逐条检查那些**不报错、不记日志、行为悄悄变得不对**的问题,
CI 会跑,命中 ERROR 即失败。这里不写条数——写死的数字会悄悄过时,
真正的清单是 `tools/checksilent/checks.go``runChecks` 跑的那几个:
| 检查 | 级别 | 静默后果 |
|---|---|---|
| `modeltime-mix` | ERROR | 两个 `ModelTime` 混用,整张表查不到数据 |
| `menu-sort-overflow` | ERROR | 菜单 `sort` 超 127MySQL tinyint 拒绝写入,迁移中断 |
| `config-value-truncation` | ERROR | `sys_config.config_value` 超 255 字符被静默截断 |
| `menu-id-collision` | ERROR | 两个模块硬编码同一菜单 ID,互相覆盖 |
| `contract-import-boundary` | ERROR | 契约包 import `app/`,应用无法独立编译 |
| `contract-shim-alias` | ERROR | 契约薄壳写成 defined type 而非别名,方法集丢失,本仓可能照常编译、第三方应用编译不过 |
| `datascope-route-unguarded` | ERROR | handler 读调用方的数据权限,而注册它的路由组没装提供权限的中间件。取不到时拿到零值、走 fail-closed 分支,查询被塞进 `1 = 0`:接口对确实存在的行返回「查不到」,且只在 `enabledp: true` 的部署上出现 |
| `shutdown-budget-overruns-grace` | ERROR / WARN | `settings.yml``extend.shutdown` 预算(含清单里的 `preStop`)放不进自带 k8s 清单的 `terminationGracePeriodSeconds`,SIGKILL 在清理回调跑到一半时到达 |
| `docker-stop-cuts-shutdown-short` | ERROR / WARN | 停止容器的两条路径——脚本/工作流里的 `docker stop`,和 `docker-compose.yml``stop_grace_period`——没写或写得不够关闭预算用。两边默认都是 10 秒,而这个数字离命令很远,调大预算的人不会想起它 |
| `menu-name-mismatch` | WARN | 菜单名与前端组件 `name` 不一致,keep-alive 缓存静默失效 |
两条关闭预算检查分两级,用的是同一条算术和同一个 5 秒边际:真的超限报 ERROR,
放得进但余量不足 5 秒报 WARN。余量不足做 WARN 不做 ERROR,是因为那是个技术上
跑得通的配置——**一条在正确配置下也会响的 ERROR,训练的是忽略它**。
最后一条要跨仓库比对,只能做正则启发式,因此是 WARN,**不影响退出码**,
且默认跳过;要跑它得指定前端目录:
```bash
make checksilent UI_DIR=../go-admin-ui/src
```
升级门槛:连续 2 个发版周期零误报后转为 ERROR。
## 提交规范
格式 `type+emoji: 描述`
`feat✨` `fix🐛` `style💄` `docs📝` `perf👌` `test✅` `refactor🎨` `chore🔧`
一个提交只做一件事。改动跨越多个语义时拆分提交,不要混在一起。
## 红线
- 不使用全局 DB 变量,一律用 `e.Orm`(来自请求上下文,多租户依赖它)
- 不在 Service 中引用 `gin.Context`
- 生产部署前确认 `mode: prod` 且已修改 `jwt.secret`(dev 模式下 token 几乎不过期)
- 不提交 `config/settings.yml` 中的真实凭据
+29 -21
View File
@@ -1,26 +1,34 @@
FROM golang:alpine as builder
MAINTAINER lwnmengjing
ENV GOPROXY https://goproxy.cn/
WORKDIR /go/release
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk update && apk add tzdata
COPY go.mod ./go.mod
RUN go mod download
COPY . .
RUN pwd && ls
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -a -installsuffix cgo -o go-admin .
FROM alpine FROM alpine
COPY --from=builder /go/release/go-admin / # ENV GOPROXY https://goproxy.cn/
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
# Runtime packages only.
#
# gcc and g++ used to be installed here and were 273MB of a 381MB image. The
# binary this image runs is compiled and linked before the image is built and
# arrives as a COPY, so nothing in the container ever invokes a compiler -
# there is no toolchain to drive it with either, since Go itself is not
# installed.
#
# That layer was also why a host could not share storage between images. apk
# resolves against an index that moves, so the layer digest differed on every
# build and no two images shared it: a host keeping one image per deployed
# commit stored a private 273MB copy each time. 68 of them filled the disk
# and the next deployment could not pull.
#
# libc6-compat stays. Nothing measured needs it - the binary CI produces is
# statically linked, and a container built without libc6-compat resolves a
# hostname and opens a database connection exactly as one built with it - but
# it is half a megabyte and it covers a ./main that was linked dynamically,
# which this Dockerfile has no way to check.
RUN apk add --no-cache ca-certificates tzdata libc6-compat
ENV TZ Asia/Shanghai
COPY ./main /main
COPY ./config/settings.demo.yml /config/settings.yml
COPY ./go-admin-db.db /go-admin-db.db
EXPOSE 8000 EXPOSE 8000
RUN chmod +x /main
CMD ["/go-admin","server","-c", "/config/settings.yml"] CMD ["/main","server","-c", "/config/settings.yml"]
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2020 wenjianzhang Copyright (c) 2026 go-admin-team
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+101 -5
View File
@@ -2,13 +2,109 @@ PROJECT:=go-admin
.PHONY: build .PHONY: build
build: build:
CGO_ENABLED=0 go build -o go-admin main.go CGO_ENABLED=0 go build -ldflags="-w -s" -a -installsuffix "" -o go-admin .
# make build-linux
build-linux:
@docker build -t go-admin:latest .
@echo "build successful"
build-sqlite: build-sqlite:
go build -tags sqlite3 -o go-admin main.go go build -tags sqlite3 -ldflags="-w -s" -a -installsuffix -o go-admin .
#.PHONY: test
#test: # make run
# go test -v ./... -cover run:
# delete go-admin-api container
#
# stop then rm, rather than `rm -f`. The force flag kills a running
# container with SIGKILL and no grace at all, so restarting locally cut
# short every shutdown this application does - the drain window was never
# once reached on a developer's machine. --timeout has to cover
# extend.shutdown's drain + server + cleanup; checksilent's
# docker-stop-cuts-shutdown-short check compares it against
# config/settings.yml. On a container that has already stopped, stop is a
# no-op and the removal is unchanged.
@if [ $(shell docker ps -aq --filter name=go-admin --filter publish=8000) ]; then docker stop --timeout 30 go-admin && docker rm go-admin; fi
# 启动方法一 run go-admin-api container docker-compose 启动方式
# 进入到项目根目录 执行 make run 命令
@docker-compose up -d
# 启动方式二 docker run 这里注意-v挂载的宿主机的地址改为部署时的实际绝对路径
#@docker run --name=go-admin -p 8000:8000 -v /home/code/go/src/go-admin/go-admin/config:/go-admin-api/config -v /home/code/go/src/go-admin/go-admin-api/static:/go-admin/static -v /home/code/go/src/go-admin/go-admin/temp:/go-admin-api/temp -d --restart=always go-admin:latest
@echo "go-admin service is running..."
# delete Tag=<none> 的镜像
@docker image prune -f
@docker ps -a | grep "go-admin"
stop:
# delete go-admin-api container
@if [ $(shell docker ps -aq --filter name=go-admin --filter publish=8000) ]; then docker-compose down; fi
#@if [ $(shell docker ps -aq --filter name=go-admin --filter publish=8000) ]; then docker rm -f go-admin; fi
#@echo "go-admin stop success"
# -race is worth the extra minute here: common/actions reuses model instances
# across concurrent requests, so a Generate() that returns in place instead of
# a copy leaks data between them - and that is invisible to a single-threaded
# test run.
.PHONY: test
test:
go test -race -cover ./...
# The end-to-end install, which `test` above cannot reach: test/e2e-apporder
# is its own module, so `./...` in this one does not include it. It builds a
# go-admin binary with the example application linked in and drives
# `migrate install` / `migrate uninstall` against a real database.
#
# Its own target rather than a line in the CI workflow, so the one thing in
# the build that exercises installing an application is also the one thing
# somebody can run before pushing.
.PHONY: test-e2e
test-e2e:
cd test/e2e-apporder && go test ./... -count=1
# Reports the failures that do not announce themselves - see
# tools/checksilent. Exits non-zero on an ERROR; the one WARN-level check
# prints and does not fail the build.
#
# Pass UI_DIR to enable the cross-repository menu-name check, which is skipped
# without it: make checksilent UI_DIR=../go-admin-ui/src
.PHONY: checksilent
checksilent:
ifdef UI_DIR
go run ./tools/checksilent -ui-dir $(UI_DIR)
else
go run ./tools/checksilent
endif
# gofmt as a gate, not a rewrite. CI cannot commit, and a target that quietly
# reformats hides what it touched, so this reports and fails instead. `gofmt -l`
# prints the files it would rewrite and nothing at all when there are none, so
# that list is both the failure message and the instructions for fixing it.
#
# The tree reached zero unformatted files once; without something holding it
# there it drifts back, which is how the previous batch grew to 26 files -
# mostly a missing newline at the end of the file, which no reviewer notices.
.PHONY: fmt-check
fmt-check:
@unformatted=$$(gofmt -l .); \
if [ -n "$$unformatted" ]; then \
echo "gofmt would rewrite these files. Run 'gofmt -w .' and commit the result:"; \
echo "$$unformatted"; \
exit 1; \
fi
#.PHONY: docker #.PHONY: docker
#docker: #docker:
# docker build . -t go-admin:latest # docker build . -t go-admin:latest
# make deploy
deploy:
#@git checkout master
#@git pull origin master
make build-linux
make run
+116 -59
View File
@@ -1,27 +1,30 @@
# go-admin # go-admin
<img align="right" width="320" src="https://gitee.com/mydearzwj/image/raw/master/img/go-admin.svg"> <img align="right" width="320" src="https://doc-image.zhangwj.com/img/go-admin.svg">
[![Build Status](https://github.com/wenjianzhang/go-admin/workflows/build/badge.svg)](https://github.com/go-admin-team/go-admin) [![Build Status](https://github.com/go-admin-team/go-admin/actions/workflows/go.yml/badge.svg?branch=master)](https://github.com/go-admin-team/go-admin)
[![Release](https://img.shields.io/github/release/go-admin-team/go-admin.svg?style=flat-square)](https://github.com/go-admin-team/go-admin/releases) [![Release](https://img.shields.io/github/release/go-admin-team/go-admin.svg?style=flat-square)](https://github.com/go-admin-team/go-admin/releases)
[![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/go-admin-team/go-admin) [![License](https://img.shields.io/github/license/go-admin-team/go-admin.svg)](https://github.com/go-admin-team/go-admin)
[English](https://github.com/go-admin-team/go-admin/blob/master/README.md) | 简体中文 [English](https://github.com/go-admin-team/go-admin/blob/master/README.md) | 简体中文 | [繁體中文](https://github.com/go-admin-team/go-admin/blob/master/README.zh-TW.md) | [日本語](https://github.com/go-admin-team/go-admin/blob/master/README.ja-JP.md)
基于Gin + Vue + Element UI OR Arco Design OR Ant Design的前后端分离权限管理系统,系统初始化极度简单,只需要配置文件中,修改数据库连接,系统支持多指令操作,迁移指令可以让初始化数据库信息变得更简单,服务指令可以很简单的启动api服务
基于Gin + Vue + Element UI的前后端分离权限管理系统,系统初始化极度简单,只需要配置文件中,修改数据库连接,系统支持多指令操作,迁移指令可以让初始化数据库信息变得更简单,服务指令可以很简单的启动api服务 [在线文档](https://www.go-admin.pro)
[在线文档](https://doc.go-admin.dev)
[github在线文档](https://wenjianzhang.github.io)
[gitee在线文档](http://mydearzwj.gitee.io/go-admin-doc/)
[前端项目](https://github.com/go-admin-team/go-admin-ui) [前端项目](https://github.com/go-admin-team/go-admin-ui)
[视频教程](https://space.bilibili.com/565616721/channel/detail?cid=125737) [视频教程](https://space.bilibili.com/565616721/channel/detail?cid=125737)
## 🎬 在线体验
Element Plus vue3 体验:[https://vue.go-admin.pro](https://vue.go-admin.pro/#/login)
> ⚠️⚠️⚠️ 账号 / 密码: admin / 123456
antd 体验(go-admin-pro):[https://antd.go-admin.pro](https://antd.go-admin.pro/)
> ⚠️⚠️⚠️ 账号 / 密码: admin / 123456
## ✨ 特性 ## ✨ 特性
- 遵循 RESTful API 设计规范 - 遵循 RESTful API 设计规范
@@ -34,7 +37,7 @@
- 支持 Swagger 文档(基于swaggo) - 支持 Swagger 文档(基于swaggo)
- 基于 GORM 的数据库存储,可扩展多种类型数据库 - 基于 GORM 的数据库存储,可扩展多种类型数据库
- 配置文件简单的模型映射,快速能够得到想要的配置 - 配置文件简单的模型映射,快速能够得到想要的配置
@@ -44,11 +47,13 @@
- 多指令模式 - 多指令模式
- TODO: 单元测试 - 多租户的支持
- TODO: 单元测试
## 🎁 内置 ## 🎁 内置
1. 多租户:系统默认支持多租户,按库分离,一个库一个租户。
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。 1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。 2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
3. 岗位管理:配置系统用户所属担任职务。 3. 岗位管理:配置系统用户所属担任职务。
@@ -63,6 +68,7 @@
1. 表单构建:自定义页面样式,拖拉拽实现页面布局。 1. 表单构建:自定义页面样式,拖拉拽实现页面布局。
1. 服务监控:查看一些服务器的基本信息。 1. 服务监控:查看一些服务器的基本信息。
1. 内容管理:demo功能,下设分类管理、内容管理。可以参考使用方便快速入门。 1. 内容管理:demo功能,下设分类管理、内容管理。可以参考使用方便快速入门。
1. 定时任务:自动化任务,目前支持接口调用和函数调用。
## 准备工作 ## 准备工作
@@ -72,11 +78,11 @@
### 轻松实现go-admin写出第一个应用 - 文档教程 ### 轻松实现go-admin写出第一个应用 - 文档教程
[步骤一 - 基础内容介绍](http://doc.zhangwj.com/go-admin-site/guide/intro/tutorial01.html) [步骤一 - 基础内容介绍](https://www.go-admin.pro/guide/intro/tutorial01.html)
[步骤二 - 实际应用 - 编写增删改查](http://doc.zhangwj.com/go-admin-site/guide/intro/tutorial02.html) [步骤二 - 实际应用 - 编写增删改查](https://www.go-admin.pro/guide/intro/tutorial02.html)
### 手把手教你从入门到放弃 - 视频教程 ### 手把手教你从入门到放弃 - 视频教程
[如何启动go-admin](https://www.bilibili.com/video/BV1z5411x7JG) [如何启动go-admin](https://www.bilibili.com/video/BV1z5411x7JG)
@@ -94,11 +100,18 @@
[go-admin数据权限使用说明](https://www.bilibili.com/video/BV1LK4y1s71e) [必看] [go-admin数据权限使用说明](https://www.bilibili.com/video/BV1LK4y1s71e) [必看]
**如有问题请先看上述使用文档和文章,若不能满足,欢迎 issue 和 pr ,视频教程和文档持续更新中** **如有问题请先看上述使用文档和文章,若不能满足,欢迎 issue 和 pr ,视频教程和文档持续更新中**
## 📦 本地开发 ## 📦 本地开发
### 环境要求
go 1.26.5
node版本: v22+(推荐 v24 LTS
包管理器: pnpm v9+UI 项目使用 pnpm
### 开发目录创建 ### 开发目录创建
```bash ```bash
@@ -121,7 +134,6 @@ git clone https://github.com/go-admin-team/go-admin-ui.git
``` ```
### 启动说明 ### 启动说明
#### 服务端启动说明 #### 服务端启动说明
@@ -130,19 +142,22 @@ git clone https://github.com/go-admin-team/go-admin-ui.git
# 进入 go-admin 后端项目 # 进入 go-admin 后端项目
cd ./go-admin cd ./go-admin
# 更新整理依赖
go mod tidy
# 编译项目 # 编译项目
go build go build
# 修改配置 # 修改配置
# 文件路径 go-admin/config/settings.yml # 文件路径 go-admin/config/settings.yml
vi ./config/setting.yml vi ./config/settings.yml
# 1. 配置文件中修改数据库信息 # 1. 配置文件中修改数据库信息
# 注意: settings.database 下对应的配置数据 # 注意: settings.database 下对应的配置数据
# 2. 确认log路径 # 2. 确认log路径
``` ```
:::tip ⚠️注意 在windows环境如果没有安装中CGO,会出现这个问题; ⚠️注意 在windows环境如果没有安装中CGO,会出现这个问题;
```bash ```bash
E:\go-admin>go build E:\go-admin>go build
@@ -158,19 +173,18 @@ D:\Code\go-admin>go build
cgo: exec gcc: exec: "gcc": executable file not found in %PATH% cgo: exec gcc: exec: "gcc": executable file not found in %PATH%
``` ```
[解决cgo问题进入](https://doc.go-admin.dev/guide/other/faq.html#_5-cgo-exec-missing-cc-exec-missing-cc-file-does-not-exist) [解决cgo问题进入](https://www.go-admin.pro/zh-CN/guide/faq#cgo-%E7%9A%84%E9%97%AE%E9%A2%98)
:::
#### 初始化数据库,以及服务启动 #### 初始化数据库,以及服务启动
``` bash ``` bash
# 首次配置需要初始化数据库资源信息 # 首次配置需要初始化数据库资源信息
# macOS or linux 下使用 # macOS or linux 下使用
$ ./go-admin migrate -c=config/settings.dev.yml $ ./go-admin migrate -c config/settings.dev.yml
# ⚠️注意:windows 下使用 # ⚠️注意:windows 下使用
$ go-admin.exe migrate -c=config/settings.dev.yml $ go-admin.exe migrate -c config/settings.dev.yml
# 启动项目,也可以用IDE进行调试 # 启动项目,也可以用IDE进行调试
@@ -182,6 +196,13 @@ $ ./go-admin server -c config/settings.yml
$ go-admin.exe server -c config/settings.yml $ go-admin.exe server -c config/settings.yml
``` ```
#### sys_api 表的数据如何添加
在项目启动时,使用`-a true` 系统会自动添加缺少的接口数据
```bash
./go-admin server -c config/settings.yml -a true
```
#### 使用docker 编译启动 #### 使用docker 编译启动
```shell ```shell
@@ -193,8 +214,6 @@ docker build -t go-admin .
docker run --name go-admin -p 8000:8000 -v /config/settings.yml:/config/settings.yml -d go-admin-server docker run --name go-admin -p 8000:8000 -v /config/settings.yml:/config/settings.yml -d go-admin-server
``` ```
#### 文档生成 #### 文档生成
```bash ```bash
@@ -202,6 +221,7 @@ go generate
``` ```
#### 交叉编译 #### 交叉编译
```bash ```bash
# windows # windows
env GOOS=windows GOARCH=amd64 go build main.go env GOOS=windows GOARCH=amd64 go build main.go
@@ -214,45 +234,77 @@ env GOOS=linux GOARCH=amd64 go build main.go
### UI交互端启动说明 ### UI交互端启动说明
```bash ```bash
# 安装依赖 # 安装 pnpm(若未安装)
npm install npm install -g pnpm
# 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 # 安装依赖
npm install --registry=https://registry.npm.taobao.org pnpm install
# 国内网络可指定镜像源加速
pnpm install --registry=https://registry.npmmirror.com
# 启动服务 # 启动服务
npm run dev pnpm dev
``` ```
## 🎬 在线体验
> admin / 123456
演示地址:[http://www.go-admin.dev](http://www.go-admin.dev/#/login)
## 📨 互动 ## 📨 互动
<table> <table>
<tr> <tr>
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/wx.png" width="180px"></td> <td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/wx.png" width="180px"></td>
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/qq.png" width="200px"></td> <td><img src="https://doc-image.zhangwj.com/img/qrcode_for_gh_b798dc7db30c_258.jpg" width="180px"></td>
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/qq2.png" width="200px"></td> <td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/qq2.png" width="200px"></td>
<td><a href="https://space.bilibili.com/565616721">wenjianzhang</a></td>
</tr> </tr>
<tr> <tr>
<td>微信</td> <td>微信</td>
<td>此群已满</td> <td>公众号🔥🔥🔥</td>
<td><a target="_blank" href="https://shang.qq.com/wpa/qunwpa?idkey=0f2bf59f5f2edec6a4550c364242c0641f870aa328e468c4ee4b7dbfb392627b"><img border="0" src="https://pub.idqqimg.com/wpa/images/group.png" alt="go-admin技术交流乙号" title="go-admin技术交流乙号"></a></td> <td><a target="_blank" href="https://shang.qq.com/wpa/qunwpa?idkey=0f2bf59f5f2edec6a4550c364242c0641f870aa328e468c4ee4b7dbfb392627b"><img border="0" src="https://pub.idqqimg.com/wpa/images/group.png" alt="go-admin技术交流乙号" title="go-admin技术交流乙号"></a></td>
<td>哔哩哔哩🔥🔥🔥</td>
</tr> </tr>
</table> </table>
## 💎 主要成员 ## 💎 贡献者
<a href="https://github.com/wenjianzhang"> <img src="https://avatars.githubusercontent.com/u/3890175?s=460&u=20eac63daef81588fbac611da676b99859319251&v=4" width="80px"></a>
<a href="https://github.com/lwnmengjing"> <img src="https://avatars.githubusercontent.com/u/12806223?s=400&u=a89272dce50100b77b4c0d5c81c718bf78ebb580&v=4" width="80px"></a>
<a href="https://github.com/chengxiao"> <img src="https://avatars.githubusercontent.com/u/1379545?s=460&u=557da5503d0ac4a8628df6b4075b17853d5edcd9&v=4" width="80px"></a>
<a href="https://github.com/bing127"> <img src="https://avatars.githubusercontent.com/u/31166183?s=460&u=c085bff88df10bb7676c8c0351ba9dcd031d1fb3&v=4" width="80px"></a>
<span style="margin: 0 5px;" ><a href="https://github.com/wenjianzhang" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/3890175?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/G-Akiraka" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/45746659?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/lwnmengjing" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/12806223?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/bing127" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/31166183?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/chengxiao" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/1379545?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/NightFire0307" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/19854086?v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/appleboy" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/21979?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/ninstein" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/580303?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/kikiyou" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/17959053?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/horizonzy" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/22524871?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/nodece" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/16235121?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/stephenzhang0713" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/18169290?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/zhouxixi-dev" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/100399679?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/Jalins" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/31172582?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/wxxiong6" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/6983441?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/GizmoOAO" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/20385106?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/bestgopher" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/36840497?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/wxb1207" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/20775558?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/misakichan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/16569274?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/zhuxuyang" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/19301024?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/mss-boot" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/109259065?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/AuroraV" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/37330199?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/Vingurzhou" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/57127283?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/haimait" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/40926384?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/zyd" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/3446278?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/infnan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/38274826?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/d1y" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/45585937?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/qlijin" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/515900?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/stepway
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/9927079?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/NaturalGao
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/43291304?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/DemoLiang
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/23476007?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/jfcg
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/1410597?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/Nicole0724
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/10487328?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
## JetBrains 开源证书支持 ## JetBrains 开源证书支持
@@ -260,18 +312,22 @@ npm run dev
<a href="https://www.jetbrains.com/?from=kubeadm-ha" target="_blank"><img src="https://raw.githubusercontent.com/panjf2000/illustrations/master/jetbrains/jetbrains-variant-4.png" width="250" align="middle"/></a> <a href="https://www.jetbrains.com/?from=kubeadm-ha" target="_blank"><img src="https://raw.githubusercontent.com/panjf2000/illustrations/master/jetbrains/jetbrains-variant-4.png" width="250" align="middle"/></a>
## 🤝 特别感谢 ## 🤝 特别感谢
1. [chengxiao](https://github.com/chengxiao)
2. [gin](https://github.com/gin-gonic/gin) 1. [ant-design](https://github.com/ant-design/ant-design)
2. [casbin](https://github.com/casbin/casbin) 2. [ant-design-pro](https://github.com/ant-design/ant-design-pro)
2. [spf13/viper](https://github.com/spf13/viper) 2. [arco-design](https://github.com/arco-design/arco-design)
2. [gorm](https://github.com/jinzhu/gorm) 2. [arco-design-pro](https://github.com/arco-design/arco-design-pro)
2. [gin-swagger](https://github.com/swaggo/gin-swagger) 4. [gin](https://github.com/gin-gonic/gin)
2. [jwt-go](https://github.com/dgrijalva/jwt-go) 5. [casbin](https://github.com/casbin/casbin)
2. [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) 6. [spf13/viper](https://github.com/spf13/viper)
2. [ruoyi-vue](https://gitee.com/y_project/RuoYi-Vue) 7. [gorm](https://github.com/go-gorm/gorm)
2. [form-generator](https://github.com/JakHuang/form-generator) 8. [gin-swagger](https://github.com/swaggo/gin-swagger)
9. [golang-jwt](https://github.com/golang-jwt/jwt)
10. [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
11. [ruoyi-vue](https://gitee.com/y_project/RuoYi-Vue)
12. [form-generator](https://github.com/JakHuang/form-generator)
## 🤟 打赏 ## 🤟 打赏
@@ -280,10 +336,11 @@ npm run dev
<img class="no-margin" src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/pay.png" height="200px" > <img class="no-margin" src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/pay.png" height="200px" >
## 🤝 链接 ## 🤝 链接
[Go开发者成长线路图](http://www.golangroadmap.com/)
- [mss-boot-io](https://docs.mss-boot-io.top/)
## 🔑 License ## 🔑 License
[MIT](https://github.com/go-admin-team/go-admin/blob/master/LICENSE.md) [MIT](https://github.com/go-admin-team/go-admin/blob/master/LICENSE.md)
Copyright (c) 2020 wenjianzhang Copyright (c) 2026 wenjianzhang
+344
View File
@@ -0,0 +1,344 @@
# go-admin
<img align="right" width="320" src="https://doc-image.zhangwj.com/img/go-admin.svg">
[![Build Status](https://github.com/go-admin-team/go-admin/actions/workflows/go.yml/badge.svg?branch=master)](https://github.com/go-admin-team/go-admin)
[![Release](https://img.shields.io/github/release/go-admin-team/go-admin.svg?style=flat-square)](https://github.com/go-admin-team/go-admin/releases)
[![License](https://img.shields.io/github/license/go-admin-team/go-admin.svg)](https://github.com/go-admin-team/go-admin)
[English](https://github.com/go-admin-team/go-admin/blob/master/README.md) | [简体中文](https://github.com/go-admin-team/go-admin/blob/master/README.Zh-cn.md) | [繁體中文](https://github.com/go-admin-team/go-admin/blob/master/README.zh-TW.md) | 日本語
Gin + Vue + Element UI / Arco Design / Ant Design による、フロントエンドとバックエンドを分離した権限管理システムです。初期化は非常に簡単で、設定ファイルのデータベース接続情報を変更するだけで動作します。複数のコマンドに対応しており、マイグレーションコマンドでデータベースの初期化が容易になり、サーバーコマンドで API を手軽に起動できます。
[オンラインドキュメント](https://www.go-admin.pro)
[フロントエンドプロジェクト](https://github.com/go-admin-team/go-admin-ui)
[動画チュートリアル](https://space.bilibili.com/565616721/channel/detail?cid=125737)
## 🎬 オンラインデモ
Element Plus vue3 デモ:[https://vue.go-admin.pro](https://vue.go-admin.pro/#/login)
> ⚠️⚠️⚠️ アカウント / パスワード: admin / 123456
antd デモ(go-admin-pro):[https://antd.go-admin.pro](https://antd.go-admin.pro/)
> ⚠️⚠️⚠️ アカウント / パスワード: admin / 123456
## ✨ 特徴
- RESTful API の設計規約に準拠
- GIN WEB API フレームワークをベースに、豊富なミドルウェアを提供(ユーザー認証、CORS、アクセスログ、トレース ID など)
- Casbin による RBAC アクセス制御モデル
- JWT 認証
- Swagger ドキュメントに対応(swaggo ベース)
- GORM によるデータベース永続化、複数種類のデータベースに拡張可能
- 設定ファイルからモデルへの単純なマッピングで、必要な設定をすぐに取得
- コード生成ツール
- フォームビルダー
- マルチコマンド方式
- マルチテナント対応
- TODO: ユニットテスト
## 🎁 標準機能
1. マルチテナント:デフォルトで対応。データベース単位で分離し、1 データベースにつき 1 テナント。
1. ユーザー管理:システムの操作者であるユーザーの設定を行います。
2. 部門管理:組織構造(会社・部門・グループ)を設定します。ツリー構造で表示し、データ権限に対応します。
3. 役職管理:ユーザーが担当する職務を設定します。
4. メニュー管理:メニュー、操作権限、ボタン権限識別子、API 権限などを設定します。
5. ロール管理:ロールへのメニュー権限の割り当て、および組織単位でのデータ範囲権限の設定を行います。
6. 辞書管理:システム内で頻繁に使う固定的なデータを管理します。
7. パラメータ管理:よく使うパラメータを動的に設定します。
8. 操作ログ:正常系の操作ログと異常情報のログを記録・検索します。
9. ログインログ:ログイン履歴を記録・検索します。ログイン異常も含みます。
1. API ドキュメント:業務コードから API ドキュメントを自動生成します。
1. コード生成:テーブル定義から CRUD 業務を生成します。すべて画面上で操作でき、基本的な業務をコードなしで実現できます。
1. フォームビルダー:ページのスタイルをカスタマイズし、ドラッグ&ドロップでレイアウトを作成します。
1. サービス監視:サーバーの基本情報を確認します。
1. コンテンツ管理:デモ機能。カテゴリ管理とコンテンツ管理を含み、入門用の参考実装として利用できます。
1. スケジュールタスク:自動実行タスク。現在は API 呼び出しと関数呼び出しに対応しています。
## 事前準備
ローカルに [go] [gin] [node](http://nodejs.org/) と [git](https://git-scm.com/) をインストールしてください。
ダウンロードから使いこなすまでを解説した動画とドキュメントのチュートリアルを用意しています。本プロジェクトを試す前に、まずこれらに目を通すことを強くおすすめします。
### go-admin で最初のアプリケーションを作る - ドキュメント
[ステップ 1 - 基礎の紹介](https://www.go-admin.pro/guide/intro/tutorial01.html)
[ステップ 2 - 実践 - CRUD を書く](https://www.go-admin.pro/guide/intro/tutorial02.html)
### 動画チュートリアル
[go-admin の起動方法](https://www.bilibili.com/video/BV1z5411x7JG)
[生成ツールで業務を手軽に実装する](https://www.bilibili.com/video/BV1Dg4y1i79D)
[v1.1.0 のコード生成ツール](https://www.bilibili.com/video/BV1N54y1i71P) [応用]
[マルチコマンドでの起動方法と IDE 設定](https://www.bilibili.com/video/BV1Fg4y1q7ph)
[go-admin のメニュー設定](https://www.bilibili.com/video/BV1Wp4y1D715) [必見]
[メニュー情報と API 情報の設定方法](https://www.bilibili.com/video/BV1zv411B7nG) [必見]
[go-admin の権限設定](https://www.bilibili.com/video/BV1rt4y197d3) [必見]
[go-admin のデータ権限](https://www.bilibili.com/video/BV1LK4y1s71e) [必見]
**不明点はまず上記のドキュメントと記事をご確認ください。解決しない場合は issue や pr をお寄せください。動画とドキュメントは継続的に更新しています**
## 📦 ローカル開発
### 動作要件
go 1.26.5
node バージョン: v22 以上(v24 LTS 推奨)
パッケージマネージャー: pnpm v9 以上(UI プロジェクトは pnpm を使用)
### 開発ディレクトリの作成
```bash
# 開発ディレクトリを作成
mkdir goadmin
cd goadmin
```
### コードの取得
> 重要:2 つのプロジェクトは同じディレクトリに配置してください。
```bash
# バックエンドのコードを取得
git clone https://github.com/go-admin-team/go-admin.git
# フロントエンドのコードを取得
git clone https://github.com/go-admin-team/go-admin-ui.git
```
### 起動方法
#### サーバーの起動
```bash
# go-admin バックエンドプロジェクトへ移動
cd ./go-admin
# 依存関係を整理
go mod tidy
# ビルド
go build
# 設定を変更
# ファイルパス go-admin/config/settings.yml
vi ./config/settings.yml
# 1. 設定ファイル内のデータベース情報を変更
# 注意: settings.database 配下の設定項目
# 2. log のパスを確認
```
⚠️注意 Windows 環境で CGO が未導入の場合、次のエラーが発生します。
```bash
E:\go-admin>go build
# github.com/mattn/go-sqlite3
cgo: exec /missing-cc: exec: "/missing-cc": file does not exist
```
or
```bash
D:\Code\go-admin>go build
# github.com/mattn/go-sqlite3
cgo: exec gcc: exec: "gcc": executable file not found in %PATH%
```
[cgo の問題の解決方法はこちら](https://www.go-admin.pro/zh-CN/guide/faq#cgo-%E7%9A%84%E9%97%AE%E9%A2%98)
#### データベースの初期化とサービス起動
``` bash
# 初回はデータベースのリソース情報を初期化する必要があります
# macOS または linux の場合
$ ./go-admin migrate -c config/settings.dev.yml
# ⚠️注意: windows の場合
$ go-admin.exe migrate -c config/settings.dev.yml
# プロジェクトを起動します。IDE からデバッグ実行することもできます
# macOS または linux の場合
$ ./go-admin server -c config/settings.yml
# ⚠️注意: windows の場合
$ go-admin.exe server -c config/settings.yml
```
#### sys_api テーブルへのデータ追加方法
起動時に `-a true` を付けると、不足している API データが自動的に追加されます。
```bash
./go-admin server -c config/settings.yml -a true
```
#### docker でのビルドと起動
```shell
# イメージをビルド
docker build -t go-admin .
# コンテナを起動します。1 つ目の go-admin はコンテナ名、2 つ目はイメージ名です
# -v は設定ファイルのマウント ローカルパス:コンテナ内パス
docker run --name go-admin -p 8000:8000 -v /config/settings.yml:/config/settings.yml -d go-admin-server
```
#### ドキュメント生成
```bash
go generate
```
#### クロスコンパイル
```bash
# windows
env GOOS=windows GOARCH=amd64 go build main.go
# or
# linux
env GOOS=linux GOARCH=amd64 go build main.go
```
### UI 側の起動方法
```bash
# pnpm をインストール(未導入の場合)
npm install -g pnpm
# 依存関係をインストール
pnpm install
# 中国本土のネットワークではミラーを指定すると高速化できます
pnpm install --registry=https://registry.npmmirror.com
# 開発サーバーを起動
pnpm dev
```
## 📨 コミュニティ
<table>
<tr>
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/wx.png" width="180px"></td>
<td><img src="https://doc-image.zhangwj.com/img/qrcode_for_gh_b798dc7db30c_258.jpg" width="180px"></td>
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/qq2.png" width="200px"></td>
<td><a href="https://space.bilibili.com/565616721">wenjianzhang</a></td>
</tr>
<tr>
<td>微信</td>
<td>公众号🔥🔥🔥</td>
<td><a target="_blank" href="https://shang.qq.com/wpa/qunwpa?idkey=0f2bf59f5f2edec6a4550c364242c0641f870aa328e468c4ee4b7dbfb392627b"><img border="0" src="https://pub.idqqimg.com/wpa/images/group.png" alt="go-admin技术交流乙号" title="go-admin技术交流乙号"></a></td>
<td>哔哩哔哩🔥🔥🔥</td>
</tr>
</table>
## 💎 コントリビューター
<span style="margin: 0 5px;" ><a href="https://github.com/wenjianzhang" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/3890175?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/G-Akiraka" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/45746659?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/lwnmengjing" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/12806223?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/bing127" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/31166183?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/chengxiao" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/1379545?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/NightFire0307" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/19854086?v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/appleboy" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/21979?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/ninstein" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/580303?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/kikiyou" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/17959053?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/horizonzy" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/22524871?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/nodece" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/16235121?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/stephenzhang0713" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/18169290?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/zhouxixi-dev" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/100399679?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/Jalins" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/31172582?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/wxxiong6" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/6983441?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/GizmoOAO" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/20385106?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/bestgopher" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/36840497?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/wxb1207" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/20775558?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/misakichan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/16569274?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/zhuxuyang" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/19301024?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/mss-boot" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/109259065?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/AuroraV" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/37330199?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/Vingurzhou" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/57127283?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/haimait" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/40926384?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/zyd" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/3446278?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/infnan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/38274826?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/d1y" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/45585937?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/qlijin" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/515900?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/stepway
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/9927079?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/NaturalGao
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/43291304?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/DemoLiang
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/23476007?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/jfcg
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/1410597?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/Nicole0724
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/10487328?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
## JetBrains のオープンソースライセンス支援
`go-admin` は一貫して JetBrains 社の GoLand 統合開発環境で開発されています。**free JetBrains Open Source license(s)** による正規の無償ライセンス提供に、この場を借りて感謝を申し上げます。
<a href="https://www.jetbrains.com/?from=kubeadm-ha" target="_blank"><img src="https://raw.githubusercontent.com/panjf2000/illustrations/master/jetbrains/jetbrains-variant-4.png" width="250" align="middle"/></a>
## 🤝 謝辞
1. [ant-design](https://github.com/ant-design/ant-design)
2. [ant-design-pro](https://github.com/ant-design/ant-design-pro)
2. [arco-design](https://github.com/arco-design/arco-design)
2. [arco-design-pro](https://github.com/arco-design/arco-design-pro)
4. [gin](https://github.com/gin-gonic/gin)
5. [casbin](https://github.com/casbin/casbin)
6. [spf13/viper](https://github.com/spf13/viper)
7. [gorm](https://github.com/go-gorm/gorm)
8. [gin-swagger](https://github.com/swaggo/gin-swagger)
9. [golang-jwt](https://github.com/golang-jwt/jwt)
10. [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
11. [ruoyi-vue](https://gitee.com/y_project/RuoYi-Vue)
12. [form-generator](https://github.com/JakHuang/form-generator)
## 🤟 支援
> このプロジェクトがお役に立ちましたら、作者にジュースを一杯おごる形で応援いただけます :tropical_drink:
<img class="no-margin" src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/pay.png" height="200px" >
## 🤝 関連リンク
- [mss-boot-io](https://docs.mss-boot-io.top/)
## 🔑 License
[MIT](https://github.com/go-admin-team/go-admin/blob/master/LICENSE.md)
Copyright (c) 2026 wenjianzhang
+89 -33
View File
@@ -1,23 +1,31 @@
# go-admin # go-admin
<img align="right" width="320" src="https://gitee.com/mydearzwj/image/raw/master/img/go-admin.svg"> <img align="right" width="320" src="https://raw.githubusercontent.com/wenjianzhang/image/203c5930b9ed08d5cf2fcb4516b85e412f8e0e60/img/go-admin.svg">
[![Build Status](https://github.com/wenjianzhang/go-admin/workflows/build/badge.svg)](https://github.com/go-admin-team/go-admin) [![Build Status](https://github.com/go-admin-team/go-admin/actions/workflows/go.yml/badge.svg?branch=master)](https://github.com/go-admin-team/go-admin)
[![Release](https://img.shields.io/github/release/go-admin-team/go-admin.svg?style=flat-square)](https://github.com/go-admin-team/go-admin/releases) [![Release](https://img.shields.io/github/release/go-admin-team/go-admin.svg?style=flat-square)](https://github.com/go-admin-team/go-admin/releases)
[![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/go-admin-team/go-admin) [![License](https://img.shields.io/github/license/go-admin-team/go-admin.svg)](https://github.com/go-admin-team/go-admin)
English | [简体中文](https://github.com/go-admin-team/go-admin/blob/master/README.Zh-cn.md) English | [简体中文](https://github.com/go-admin-team/go-admin/blob/master/README.Zh-cn.md) | [繁體中文](https://github.com/go-admin-team/go-admin/blob/master/README.zh-TW.md) | [日本語](https://github.com/go-admin-team/go-admin/blob/master/README.ja-JP.md)
The front-end and back-end separation authority management system based on Gin + Vue + Element UI is extremely simple to initialize the system. You only need to modify the database connection in the configuration file. The system supports multi-instruction operations. Migration instructions can make it easier to initialize database information. Service instructions It's easy to start the api service. The front-end and back-end separation authority management system based on Gin + Vue + Element UI OR Arco Design OR Ant Design is extremely simple to initialize the system. You only need to modify the database connection in the configuration file. The system supports multi-instruction operations. Migration instructions can make it easier to initialize database information. Service instructions It's easy to start the api service.
[documentation](https://doc.go-admin.dev) [documentation](https://www.go-admin.pro)
[Front-end project](https://github.com/go-admin-team/go-admin-ui) [Front-end project](https://github.com/go-admin-team/go-admin-ui)
[Video tutorial](https://space.bilibili.com/565616721/channel/detail?cid=125737) [Video tutorial](https://space.bilibili.com/565616721/channel/detail?cid=125737)
## 🎬 Online Demo
Element Plus vue3 demo[https://vue.go-admin.pro](https://vue.go-admin.pro/#/login)
> Account / Password: admin / 123456
antd demo (go-admin-pro)[https://antd.go-admin.pro](https://antd.go-admin.pro/)
> Account / Password: admin / 123456
>
## ✨ Feature ## ✨ Feature
- Follow RESTful API design specifications - Follow RESTful API design specifications
@@ -68,9 +76,9 @@ At the same time, a series of tutorials including videos and documents are provi
### Easily implement go-admin to write the first application-documentation tutorial ### Easily implement go-admin to write the first application-documentation tutorial
[Step 1 - basic content introduction](http://doc.zhangwj.com/go-admin-site/guide/intro/tutorial01.html) [Step 1 - basic content introduction](https://www.go-admin.pro/guide/intro/tutorial01.html)
[Step 2 - Practical application - writing database operations](http://doc.zhangwj.com/go-admin-site/guide/intro/tutorial02.html) [Step 2 - Practical application - writing database operations](https://www.go-admin.pro/guide/intro/tutorial02.html)
### Teach you from getting started to giving up-video tutorial ### Teach you from getting started to giving up-video tutorial
@@ -94,6 +102,14 @@ At the same time, a series of tutorials including videos and documents are provi
## 📦 Local development ## 📦 Local development
### Environmental requirements
go 1.26.5
nodejs: v22+ (v24 LTS recommended)
package manager: pnpm v9+ (the UI project uses pnpm)
### Development directory creation ### Development directory creation
```bash ```bash
@@ -124,19 +140,22 @@ git clone https://github.com/go-admin-team/go-admin-ui.git
# Enter the go-admin backend project # Enter the go-admin backend project
cd ./go-admin cd ./go-admin
# Update dependencies
go mod tidy
# Compile the project # Compile the project
go build go build
# Change setting # Change setting
# File path go-admin/config/settings.yml # File path go-admin/config/settings.yml
vi ./config/setting.yml vi ./config/settings.yml
# 1. Modify the database information in the configuration file # 1. Modify the database information in the configuration file
# Note: The corresponding configuration data under settings.database # Note: The corresponding configuration data under settings.database
# 2. Confirm the log path # 2. Confirm the log path
``` ```
:::tip ⚠️Note that this problem will occur if CGO is not installed in the windows environment; ⚠️ Note that this problem will occur if CGO is not installed in the windows10+ environment;
```bash ```bash
E:\go-admin>go build E:\go-admin>go build
@@ -152,19 +171,17 @@ D:\Code\go-admin>go build
cgo: exec gcc: exec: "gcc": executable file not found in %PATH% cgo: exec gcc: exec: "gcc": executable file not found in %PATH%
``` ```
[Solve the cgo problem and enter](https://doc.go-admin.dev/guide/other/faq.html#_5-cgo-exec-missing-cc-exec-missing-cc-file-does-not-exist) [Solve the cgo problem and enter](https://www.go-admin.pro/guide/faq#cgo-%E7%9A%84%E9%97%AE%E9%A2%98)
:::
#### Initialize the database, and start the service #### Initialize the database, and start the service
``` bash ``` bash
# The first configuration needs to initialize the database resource information # The first configuration needs to initialize the database resource information
# Use under macOS or linux # Use under macOS or linux
$ ./go-admin migrate -c=config/settings.dev.yml $ ./go-admin migrate -c config/settings.dev.yml
# ⚠️Note: Use under windows # ⚠️Note: Use under windows
$ go-admin.exe migrate -c=config/settings.dev.yml $ go-admin.exe migrate -c config/settings.dev.yml
# Start the project, you can also use the IDE for debugging # Start the project, you can also use the IDE for debugging
# Use under macOS or linux # Use under macOS or linux
@@ -207,38 +224,73 @@ env GOOS=linux GOARCH=amd64 go build main.go
### UI interactive terminal startup instructions ### UI interactive terminal startup instructions
```bash ```bash
# Install pnpm if you don't have it
npm install -g pnpm
# Installation dependencies # Installation dependencies
npm install # or cnpm install pnpm install
# Start service # Start service
npm run dev pnpm dev
``` ```
## 🎬 Online Demo
> admin / 123456
演示地址:[http://www.go-admin.dev](http://www.go-admin.dev/#/login)
## 📨 Interactive ## 📨 Interactive
<table> <table>
<tr> <tr>
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/wx.png" width="180px"></td> <td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/wx.png" width="180px"></td>
<td><img src="https://doc-image.zhangwj.com/img/qrcode_for_gh_b798dc7db30c_258.jpg" width="180px"></td>
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/qq2.png" width="200px"></td> <td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/qq2.png" width="200px"></td>
<td><a href="https://space.bilibili.com/565616721">wenjianzhang</a></td>
</tr> </tr>
<tr> <tr>
<td>Wechat</td> <td>Wechat</td>
<td>Wechat公众号🔥🔥🔥</td>
<td><a target="_blank" href="https://shang.qq.com/wpa/qunwpa?idkey=0f2bf59f5f2edec6a4550c364242c0641f870aa328e468c4ee4b7dbfb392627b"><img border="0" src="https://pub.idqqimg.com/wpa/images/group.png" alt="go-admin技术交流乙号" title="go-admin技术交流乙号"></a></td> <td><a target="_blank" href="https://shang.qq.com/wpa/qunwpa?idkey=0f2bf59f5f2edec6a4550c364242c0641f870aa328e468c4ee4b7dbfb392627b"><img border="0" src="https://pub.idqqimg.com/wpa/images/group.png" alt="go-admin技术交流乙号" title="go-admin技术交流乙号"></a></td>
<td>bilibili🔥🔥🔥</td>
</tr> </tr>
</table> </table>
## 💎 Members ## 💎 Contributors
<a href="https://github.com/wenjianzhang"> <img src="https://avatars.githubusercontent.com/u/3890175?s=460&u=20eac63daef81588fbac611da676b99859319251&v=4" width="80px"></a> <span style="margin: 0 5px;" ><a href="https://github.com/wenjianzhang" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/3890175?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<a href="https://github.com/lwnmengjing"> <img src="https://avatars.githubusercontent.com/u/12806223?s=400&u=a89272dce50100b77b4c0d5c81c718bf78ebb580&v=4" width="80px"></a> <span style="margin: 0 5px;" ><a href="https://github.com/G-Akiraka" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/45746659?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<a href="https://github.com/chengxiao"> <img src="https://avatars.githubusercontent.com/u/1379545?s=460&u=557da5503d0ac4a8628df6b4075b17853d5edcd9&v=4" width="80px"></a> <span style="margin: 0 5px;" ><a href="https://github.com/lwnmengjing" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/12806223?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<a href="https://github.com/bing127"> <img src="https://avatars.githubusercontent.com/u/31166183?s=460&u=c085bff88df10bb7676c8c0351ba9dcd031d1fb3&v=4" width="80px"></a> <span style="margin: 0 5px;" ><a href="https://github.com/bing127" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/31166183?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/chengxiao" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/1379545?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/NightFire0307" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/19854086?v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/appleboy" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/21979?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/ninstein" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/580303?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/kikiyou" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/17959053?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/horizonzy" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/22524871?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/nodece" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/16235121?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/stephenzhang0713" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/18169290?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/zhouxixi-dev" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/100399679?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/Jalins" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/31172582?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/wxxiong6" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/6983441?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/GizmoOAO" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/20385106?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/bestgopher" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/36840497?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/wxb1207" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/20775558?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/misakichan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/16569274?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/zhuxuyang" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/19301024?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/mss-boot" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/109259065?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/AuroraV" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/37330199?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/Vingurzhou" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/57127283?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/haimait" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/40926384?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/zyd" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/3446278?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/infnan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/38274826?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/d1y" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/45585937?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/qlijin" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/515900?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/stepway
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/9927079?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/NaturalGao
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/43291304?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/DemoLiang
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/23476007?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/jfcg
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/1410597?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/Nicole0724
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/10487328?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
@@ -250,13 +302,17 @@ The `go-admin` project has always been developed in the GoLand integrated develo
## 🤝 Thanks ## 🤝 Thanks
1. [chengxiao](https://github.com/chengxiao)
1. [ant-design](https://github.com/ant-design/ant-design)
2. [ant-design-pro](https://github.com/ant-design/ant-design-pro)
2. [arco-design](https://github.com/arco-design/arco-design)
2. [arco-design-pro](https://github.com/arco-design/arco-design-pro)
2. [gin](https://github.com/gin-gonic/gin) 2. [gin](https://github.com/gin-gonic/gin)
2. [casbin](https://github.com/casbin/casbin) 2. [casbin](https://github.com/casbin/casbin)
2. [spf13/viper](https://github.com/spf13/viper) 2. [spf13/viper](https://github.com/spf13/viper)
2. [gorm](https://github.com/jinzhu/gorm) 2. [gorm](https://github.com/go-gorm/gorm)
2. [gin-swagger](https://github.com/swaggo/gin-swagger) 2. [gin-swagger](https://github.com/swaggo/gin-swagger)
2. [jwt-go](https://github.com/dgrijalva/jwt-go) 2. [golang-jwt](https://github.com/golang-jwt/jwt)
2. [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) 2. [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
2. [ruoyi-vue](https://gitee.com/y_project/RuoYi-Vue) 2. [ruoyi-vue](https://gitee.com/y_project/RuoYi-Vue)
2. [form-generator](https://github.com/JakHuang/form-generator) 2. [form-generator](https://github.com/JakHuang/form-generator)
@@ -268,10 +324,10 @@ The `go-admin` project has always been developed in the GoLand integrated develo
<img class="no-margin" src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/pay.png" height="200px" > <img class="no-margin" src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/pay.png" height="200px" >
## 🤝 Link ## 🤝 Link
[Go developer growth roadmap](http://www.golangroadmap.com/) - [mss-boot-io](https://docs.mss-boot-io.top/)
## 🔑 License ## 🔑 License
[MIT](https://github.com/go-admin-team/go-admin/blob/master/LICENSE.md) [MIT](https://github.com/go-admin-team/go-admin/blob/master/LICENSE.md)
Copyright (c) 2020 wenjianzhang Copyright (c) 2026 wenjianzhang
+344
View File
@@ -0,0 +1,344 @@
# go-admin
<img align="right" width="320" src="https://doc-image.zhangwj.com/img/go-admin.svg">
[![Build Status](https://github.com/go-admin-team/go-admin/actions/workflows/go.yml/badge.svg?branch=master)](https://github.com/go-admin-team/go-admin)
[![Release](https://img.shields.io/github/release/go-admin-team/go-admin.svg?style=flat-square)](https://github.com/go-admin-team/go-admin/releases)
[![License](https://img.shields.io/github/license/go-admin-team/go-admin.svg)](https://github.com/go-admin-team/go-admin)
[English](https://github.com/go-admin-team/go-admin/blob/master/README.md) | [简体中文](https://github.com/go-admin-team/go-admin/blob/master/README.Zh-cn.md) | 繁體中文 | [日本語](https://github.com/go-admin-team/go-admin/blob/master/README.ja-JP.md)
基於 Gin + Vue + Element UI OR Arco Design OR Ant Design 的前後端分離權限管理系統。系統初始化極為簡單,只需在設定檔中修改資料庫連線資訊即可。系統支援多指令操作:遷移指令讓資料庫初始化變得更簡單,服務指令則能輕鬆啟動 API 服務。
[線上文件](https://www.go-admin.pro)
[前端專案](https://github.com/go-admin-team/go-admin-ui)
[影片教學](https://space.bilibili.com/565616721/channel/detail?cid=125737)
## 🎬 線上體驗
Element Plus vue3 體驗:[https://vue.go-admin.pro](https://vue.go-admin.pro/#/login)
> ⚠️⚠️⚠️ 帳號 / 密碼: admin / 123456
antd 體驗(go-admin-pro):[https://antd.go-admin.pro](https://antd.go-admin.pro/)
> ⚠️⚠️⚠️ 帳號 / 密碼: admin / 123456
## ✨ 特性
- 遵循 RESTful API 設計規範
- 基於 GIN WEB API 框架,提供豐富的中介軟體支援(使用者認證、跨域、存取日誌、追蹤 ID 等)
- 基於 Casbin 的 RBAC 存取控制模型
- JWT 認證
- 支援 Swagger 文件(基於 swaggo
- 基於 GORM 的資料庫儲存,可擴充多種類型資料庫
- 設定檔簡單的模型映射,快速取得所需設定
- 程式碼產生工具
- 表單建構工具
- 多指令模式
- 多租戶的支援
- TODO: 單元測試
## 🎁 內建
1. 多租戶:系統預設支援多租戶,按資料庫分離,一個資料庫一個租戶。
1. 使用者管理:使用者是系統操作者,該功能主要完成系統使用者設定。
2. 部門管理:設定系統組織架構(公司、部門、小組),以樹狀結構呈現並支援資料權限。
3. 職位管理:設定系統使用者所擔任的職務。
4. 選單管理:設定系統選單、操作權限、按鈕權限標識、介面權限等。
5. 角色管理:角色選單權限分配、設定角色按機構進行資料範圍權限劃分。
6. 字典管理:對系統中經常使用且較為固定的資料進行維護。
7. 參數管理:對系統動態設定常用參數。
8. 操作日誌:系統正常操作的日誌記錄與查詢;系統異常資訊的日誌記錄與查詢。
9. 登入日誌:系統登入日誌記錄查詢,包含登入異常。
1. 介面文件:根據業務程式碼自動產生相關的 API 介面文件。
1. 程式碼產生:根據資料表結構產生對應的增刪改查業務,全程視覺化操作,讓基本業務可以零程式碼實現。
1. 表單建構:自訂頁面樣式,拖拉放實現頁面佈局。
1. 服務監控:檢視伺服器的基本資訊。
1. 內容管理:demo 功能,下設分類管理、內容管理,可參考使用以快速入門。
1. 排程任務:自動化任務,目前支援介面呼叫與函式呼叫。
## 準備工作
你需要在本機安裝 [go] [gin] [node](http://nodejs.org/) 和 [git](https://git-scm.com/)
同時配套了系列教學(含影片與文件),說明如何從下載到熟練使用。強烈建議先看完這些教學再來實作本專案!!!
### 輕鬆用 go-admin 寫出第一個應用 - 文件教學
[步驟一 - 基礎內容介紹](https://www.go-admin.pro/guide/intro/tutorial01.html)
[步驟二 - 實際應用 - 撰寫增刪改查](https://www.go-admin.pro/guide/intro/tutorial02.html)
### 手把手教你從入門到放棄 - 影片教學
[如何啟動 go-admin](https://www.bilibili.com/video/BV1z5411x7JG)
[使用產生工具輕鬆實現業務](https://www.bilibili.com/video/BV1Dg4y1i79D)
[v1.1.0 版本程式碼產生工具 - 釋放雙手](https://www.bilibili.com/video/BV1N54y1i71P) [進階]
[多指令啟動方式講解以及 IDE 設定](https://www.bilibili.com/video/BV1Fg4y1q7ph)
[go-admin 選單的設定說明](https://www.bilibili.com/video/BV1Wp4y1D715) [必看]
[如何設定選單資訊以及介面資訊](https://www.bilibili.com/video/BV1zv411B7nG) [必看]
[go-admin 權限設定使用說明](https://www.bilibili.com/video/BV1rt4y197d3) [必看]
[go-admin 資料權限使用說明](https://www.bilibili.com/video/BV1LK4y1s71e) [必看]
**如有問題請先參閱上述文件與文章,若仍無法解決,歡迎提出 issue 與 pr。影片教學與文件持續更新中**
## 📦 本機開發
### 環境需求
go 1.26.5
node 版本: v22+(建議 v24 LTS
套件管理器: pnpm v9+UI 專案使用 pnpm
### 建立開發目錄
```bash
# 建立開發目錄
mkdir goadmin
cd goadmin
```
### 取得程式碼
> 重點注意:兩個專案必須放在同一資料夾下;
```bash
# 取得後端程式碼
git clone https://github.com/go-admin-team/go-admin.git
# 取得前端程式碼
git clone https://github.com/go-admin-team/go-admin-ui.git
```
### 啟動說明
#### 伺服器端啟動說明
```bash
# 進入 go-admin 後端專案
cd ./go-admin
# 更新整理相依套件
go mod tidy
# 編譯專案
go build
# 修改設定
# 檔案路徑 go-admin/config/settings.yml
vi ./config/settings.yml
# 1. 在設定檔中修改資料庫資訊
# 注意: settings.database 下對應的設定資料
# 2. 確認 log 路徑
```
⚠️注意 在 Windows 環境若未安裝 CGO,會出現這個問題;
```bash
E:\go-admin>go build
# github.com/mattn/go-sqlite3
cgo: exec /missing-cc: exec: "/missing-cc": file does not exist
```
or
```bash
D:\Code\go-admin>go build
# github.com/mattn/go-sqlite3
cgo: exec gcc: exec: "gcc": executable file not found in %PATH%
```
[解決 cgo 問題請進入](https://www.go-admin.pro/zh-CN/guide/faq#cgo-%E7%9A%84%E9%97%AE%E9%A2%98)
#### 初始化資料庫,以及服務啟動
``` bash
# 首次設定需要初始化資料庫資源資訊
# macOS or linux 下使用
$ ./go-admin migrate -c config/settings.dev.yml
# ⚠️注意:windows 下使用
$ go-admin.exe migrate -c config/settings.dev.yml
# 啟動專案,也可以用 IDE 進行除錯
# macOS or linux 下使用
$ ./go-admin server -c config/settings.yml
# ⚠️注意:windows 下使用
$ go-admin.exe server -c config/settings.yml
```
#### sys_api 表的資料如何新增
在專案啟動時,使用 `-a true` 系統會自動新增缺少的介面資料
```bash
./go-admin server -c config/settings.yml -a true
```
#### 使用 docker 編譯啟動
```shell
# 編譯映像檔
docker build -t go-admin .
# 啟動容器,第一個 go-admin 是容器名稱,第二個 go-admin 是映像檔名稱
# -v 映射設定檔 本機路徑:容器路徑
docker run --name go-admin -p 8000:8000 -v /config/settings.yml:/config/settings.yml -d go-admin-server
```
#### 文件產生
```bash
go generate
```
#### 交叉編譯
```bash
# windows
env GOOS=windows GOARCH=amd64 go build main.go
# or
# linux
env GOOS=linux GOARCH=amd64 go build main.go
```
### UI 互動端啟動說明
```bash
# 安裝 pnpm(若未安裝)
npm install -g pnpm
# 安裝相依套件
pnpm install
# 中國大陸網路可指定鏡像來源加速
pnpm install --registry=https://registry.npmmirror.com
# 啟動服務
pnpm dev
```
## 📨 互動
<table>
<tr>
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/wx.png" width="180px"></td>
<td><img src="https://doc-image.zhangwj.com/img/qrcode_for_gh_b798dc7db30c_258.jpg" width="180px"></td>
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/qq2.png" width="200px"></td>
<td><a href="https://space.bilibili.com/565616721">wenjianzhang</a></td>
</tr>
<tr>
<td>微信</td>
<td>公众号🔥🔥🔥</td>
<td><a target="_blank" href="https://shang.qq.com/wpa/qunwpa?idkey=0f2bf59f5f2edec6a4550c364242c0641f870aa328e468c4ee4b7dbfb392627b"><img border="0" src="https://pub.idqqimg.com/wpa/images/group.png" alt="go-admin技术交流乙号" title="go-admin技术交流乙号"></a></td>
<td>哔哩哔哩🔥🔥🔥</td>
</tr>
</table>
## 💎 貢獻者
<span style="margin: 0 5px;" ><a href="https://github.com/wenjianzhang" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/3890175?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/G-Akiraka" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/45746659?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/lwnmengjing" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/12806223?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/bing127" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/31166183?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/chengxiao" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/1379545?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/NightFire0307" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/19854086?v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/appleboy" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/21979?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/ninstein" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/580303?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/kikiyou" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/17959053?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/horizonzy" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/22524871?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/nodece" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/16235121?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/stephenzhang0713" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/18169290?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/zhouxixi-dev" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/100399679?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/Jalins" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/31172582?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/wxxiong6" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/6983441?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/GizmoOAO" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/20385106?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/bestgopher" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/36840497?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/wxb1207" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/20775558?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/misakichan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/16569274?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/zhuxuyang" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/19301024?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/mss-boot" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/109259065?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/AuroraV" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/37330199?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/Vingurzhou" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/57127283?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/haimait" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/40926384?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/zyd" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/3446278?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/infnan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/38274826?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/d1y" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/45585937?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/qlijin" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/515900?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/stepway
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/9927079?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/NaturalGao
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/43291304?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/DemoLiang
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/23476007?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/jfcg
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/1410597?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/Nicole0724
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/10487328?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
## JetBrains 開源證書支援
`go-admin` 專案一直以來都是在 JetBrains 公司旗下的 GoLand 整合開發環境中進行開發,基於 **free JetBrains Open Source license(s)** 正版免費授權,在此表達我的謝意。
<a href="https://www.jetbrains.com/?from=kubeadm-ha" target="_blank"><img src="https://raw.githubusercontent.com/panjf2000/illustrations/master/jetbrains/jetbrains-variant-4.png" width="250" align="middle"/></a>
## 🤝 特別感謝
1. [ant-design](https://github.com/ant-design/ant-design)
2. [ant-design-pro](https://github.com/ant-design/ant-design-pro)
2. [arco-design](https://github.com/arco-design/arco-design)
2. [arco-design-pro](https://github.com/arco-design/arco-design-pro)
4. [gin](https://github.com/gin-gonic/gin)
5. [casbin](https://github.com/casbin/casbin)
6. [spf13/viper](https://github.com/spf13/viper)
7. [gorm](https://github.com/go-gorm/gorm)
8. [gin-swagger](https://github.com/swaggo/gin-swagger)
9. [golang-jwt](https://github.com/golang-jwt/jwt)
10. [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
11. [ruoyi-vue](https://gitee.com/y_project/RuoYi-Vue)
12. [form-generator](https://github.com/JakHuang/form-generator)
## 🤟 贊助
> 如果你覺得這個專案幫助到了你,可以幫作者買一杯果汁表示鼓勵 :tropical_drink:
<img class="no-margin" src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/pay.png" height="200px" >
## 🤝 連結
- [mss-boot-io](https://docs.mss-boot-io.top/)
## 🔑 License
[MIT](https://github.com/go-admin-team/go-admin/blob/master/LICENSE.md)
Copyright (c) 2026 wenjianzhang
+40
View File
@@ -0,0 +1,40 @@
package apis
import (
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/v2/captcha"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
)
type System struct {
api.Api
}
// GenerateCaptchaHandler 获取验证码
// @Summary 获取验证码
// @Description 获取验证码
// @Tags 登陆
// @Success 200 {object} response.Response{data=string,id=string,msg=string} "{"code": 200, "data": [...]}"
// @Router /api/v1/captcha [get]
func (e System) GenerateCaptchaHandler(c *gin.Context) {
if err := e.MakeContext(c).Errors; err != nil {
e.Error(500, err, "服务初始化失败!")
return
}
// The answer is deliberately discarded rather than logged. It used to be
// written at info level, which put a currently valid captcha answer in the
// application log - anyone able to read the log could bypass the check the
// captcha exists to enforce.
id, b64s, _, err := captcha.DriverDigitFunc()
if err != nil {
e.Logger.Errorf("DriverDigitFunc error, %s", err.Error())
e.Error(500, err, "验证码获取失败")
return
}
e.Custom(gin.H{
"code": 200,
"data": b64s,
"id": id,
"msg": "success",
})
}
@@ -1,4 +1,4 @@
package system package apis
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -11,13 +11,14 @@ const INDEX = `
<meta charset="utf-8"> <meta charset="utf-8">
<title>GO-ADMIN欢迎您</title> <title>GO-ADMIN欢迎您</title>
<style> <style>
body{ html,body{
margin:0; margin:0;
padding:0; padding:0;
overflow-y:hidden height:100%;
overflow-y:hidden;
} }
</style> </style>
<script src="http://libs.baidu.com/jquery/1.9.0/jquery.js"></script> <script src="https://libs.baidu.com/jquery/1.9.0/jquery.js"></script>
<script type="text/javascript"> <script type="text/javascript">
window.onerror=function(){return true;} window.onerror=function(){return true;}
$(function(){ $(function(){
@@ -28,12 +29,12 @@ $(function(){
</script> </script>
</head> </head>
<body> <body>
<iframe id="iframe" frameborder="0" src="https://doc.go-admin.dev" style="width:100%;"></iframe> <iframe id="iframe" frameborder="0" src="https://www.go-admin.pro" style="width:100%;height:100%;"></iframe>
</body> </body>
</html> </html>
` `
func HelloWorld(c *gin.Context) { func GoAdmin(c *gin.Context) {
c.Header("Content-Type", "text/html; charset=utf-8") c.Header("Content-Type", "text/html; charset=utf-8")
c.String(200, INDEX) c.String(200, INDEX)
} }
-76
View File
@@ -1,76 +0,0 @@
package monitor
import (
"runtime"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/pkg"
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/mem"
"go-admin/common/apis"
)
const (
B = 1
KB = 1024 * B
MB = 1024 * KB
GB = 1024 * MB
)
type Monitor struct {
apis.Api
}
// @Summary 系统信息
// @Description 获取JSON
// @Tags 系统信息
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/settings/serverInfo [get]
func (e Monitor) ServerInfo(c *gin.Context) {
e.Context = c
osDic := make(map[string]interface{}, 0)
osDic["goOs"] = runtime.GOOS
osDic["arch"] = runtime.GOARCH
osDic["mem"] = runtime.MemProfileRate
osDic["compiler"] = runtime.Compiler
osDic["version"] = runtime.Version()
osDic["numGoroutine"] = runtime.NumGoroutine()
osDic["ip"] = pkg.GetLocaHonst()
osDic["projectDir"] = pkg.GetCurrentPath()
dis, _ := disk.Usage("/")
diskTotalGB := int(dis.Total) / GB
diskFreeGB := int(dis.Free) / GB
diskDic := make(map[string]interface{}, 0)
diskDic["total"] = diskTotalGB
diskDic["free"] = diskFreeGB
mem, _ := mem.VirtualMemory()
memUsedMB := int(mem.Used) / GB
memTotalMB := int(mem.Total) / GB
memFreeMB := int(mem.Free) / GB
memUsedPercent := int(mem.UsedPercent)
memDic := make(map[string]interface{}, 0)
memDic["total"] = memTotalMB
memDic["used"] = memUsedMB
memDic["free"] = memFreeMB
memDic["usage"] = memUsedPercent
cpuDic := make(map[string]interface{}, 0)
cpuDic["cpuInfo"], _ = cpu.Info()
percent, _ := cpu.Percent(0, false)
cpuDic["Percent"] = pkg.Round(percent[0], 2)
cpuDic["cpuNum"], _ = cpu.Counts(false)
e.Custom(gin.H{
"code": 200,
"os": osDic,
"mem": memDic,
"cpu": cpuDic,
"disk": diskDic,
})
}
-199
View File
@@ -1,199 +0,0 @@
package public
import (
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/pkg"
"github.com/go-admin-team/go-admin-core/sdk/pkg/utils"
"github.com/google/uuid"
"go-admin/common/apis"
"go-admin/common/file_store"
)
type FileResponse struct {
Size int64 `json:"size"`
Path string `json:"path"`
FullPath string `json:"full_path"`
Name string `json:"name"`
Type string `json:"type"`
}
const path = "static/uploadfile/"
type File struct {
apis.Api
}
// @Summary 上传图片
// @Description 获取JSON
// @Tags 公共接口
// @Accept multipart/form-data
// @Param type query string true "type" (1:单图,2:多图, 3base64图片)
// @Param file formData file true "file"
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/public/uploadFile [post]
func (e File) UploadFile(c *gin.Context) {
e.Context = c
tag, _ := c.GetPostForm("type")
urlPerfix := fmt.Sprintf("http://%s/", c.Request.Host)
var fileResponse FileResponse
if tag == "" {
e.Error(500, nil, "缺少标识")
//app.Error(c, 200, errors.New(""), "缺少标识")
return
} else {
switch tag {
case "1": // 单图
var done bool
fileResponse, done = e.singleFile(c, fileResponse, urlPerfix)
if done {
return
}
e.OK(fileResponse, "上传成功")
return
case "2": // 多图
multipartFile := e.multipleFile(c, urlPerfix)
e.OK(multipartFile, "上传成功")
return
case "3": // base64
fileResponse = e.baseImg(c, fileResponse, urlPerfix)
e.OK(fileResponse, "上传成功")
}
}
}
func (e File) baseImg(c *gin.Context, fileResponse FileResponse, urlPerfix string) FileResponse {
files, _ := c.GetPostForm("file")
file2list := strings.Split(files, ",")
ddd, _ := base64.StdEncoding.DecodeString(file2list[1])
guid := uuid.New().String()
fileName := guid + ".jpg"
err := utils.IsNotExistMkDir(path)
if err != nil {
e.Error(500, errors.New(""), "初始化文件路径失败")
}
base64File := path + fileName
_ = ioutil.WriteFile(base64File, ddd, 0666)
typeStr := strings.Replace(strings.Replace(file2list[0], "data:", "", -1), ";base64", "", -1)
fileResponse = FileResponse{
Size: pkg.GetFileSize(base64File),
Path: base64File,
FullPath: urlPerfix + base64File,
Name: "",
Type: typeStr,
}
source, _ := c.GetPostForm("source")
err = thirdUpload(source, fileName, base64File)
if err != nil {
e.Error(200, errors.New(""), "上传第三方失败")
return fileResponse
}
if source != "1" {
fileResponse.Path = "https://youshikeji.oss-cn-shanghai.aliyuncs.com/img/" + fileName
fileResponse.FullPath = "https://youshikeji.oss-cn-shanghai.aliyuncs.com/img/" + fileName
}
return fileResponse
}
func (e File) multipleFile(c *gin.Context, urlPerfix string) []FileResponse {
files := c.Request.MultipartForm.File["file"]
source, _ := c.GetPostForm("source")
var multipartFile []FileResponse
for _, f := range files {
guid := uuid.New().String()
fileName := guid + utils.GetExt(f.Filename)
err := utils.IsNotExistMkDir(path)
if err != nil {
e.Error(500, errors.New(""), "初始化文件路径失败")
}
multipartFileName := path + fileName
err1 := c.SaveUploadedFile(f, multipartFileName)
fileType, _ := utils.GetType(multipartFileName)
if err1 == nil {
err := thirdUpload(source, fileName, multipartFileName)
if err != nil {
e.Error(500, errors.New(""), "上传第三方失败")
} else {
fileResponse := FileResponse{
Size: pkg.GetFileSize(multipartFileName),
Path: multipartFileName,
FullPath: urlPerfix + multipartFileName,
Name: f.Filename,
Type: fileType,
}
if source != "1" {
fileResponse.Path = "https://youshikeji.oss-cn-shanghai.aliyuncs.com/img/" + fileName
fileResponse.FullPath = "https://youshikeji.oss-cn-shanghai.aliyuncs.com/img/" + fileName
}
multipartFile = append(multipartFile, fileResponse)
}
}
}
return multipartFile
}
func (e File) singleFile(c *gin.Context, fileResponse FileResponse, urlPerfix string) (FileResponse, bool) {
files, err := c.FormFile("file")
if err != nil {
e.Error(200, errors.New(""), "图片不能为空")
return FileResponse{}, true
}
// 上传文件至指定目录
guid := uuid.New().String()
fileName := guid + utils.GetExt(files.Filename)
err = utils.IsNotExistMkDir(path)
if err != nil {
e.Error(500, errors.New(""), "初始化文件路径失败")
}
singleFile := path + fileName
_ = c.SaveUploadedFile(files, singleFile)
fileType, _ := utils.GetType(singleFile)
fileResponse = FileResponse{
Size: pkg.GetFileSize(singleFile),
Path: singleFile,
FullPath: urlPerfix + singleFile,
Name: files.Filename,
Type: fileType,
}
source, _ := c.GetPostForm("source")
err = thirdUpload(source, fileName, singleFile)
if err != nil {
e.Error(200, errors.New(""), "上传第三方失败")
return FileResponse{}, true
}
fileResponse.Path = "https://youshikeji.oss-cn-shanghai.aliyuncs.com/img/" + fileName
fileResponse.FullPath = "https://youshikeji.oss-cn-shanghai.aliyuncs.com/img/" + fileName
return fileResponse, false
}
func thirdUpload(source string, name string, path string) error {
switch source {
case "2":
return ossUpload("img/"+name, path)
case "3":
return qiniuUpload("img/"+name, path)
}
return nil
}
func ossUpload(name string, path string) error {
oss := file_store.ALiYunOSS{}
return oss.UpLoad(name, path)
}
func qiniuUpload(name string, path string) error {
oss := file_store.ALiYunOSS{}
return oss.UpLoad(name, path)
}
+148
View File
@@ -0,0 +1,148 @@
package apis
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/v2/response"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
)
type SysApi struct {
api.Api
}
// GetPage 获取接口管理列表
// @Summary 获取接口管理列表
// @Description 获取接口管理列表
// @Tags 接口管理
// @Param name query string false "名称"
// @Param title query string false "标题"
// @Param path query string false "地址"
// @Param action query string false "类型"
// @Param pageSize query int false "页条数"
// @Param pageIndex query int false "页码"
// @Success 200 {object} response.Response{data=response.Page{list=[]models.SysApi}} "{"code": 200, "data": [...]}"
// @Router /api/v1/sys-api [get]
// @Security Bearer
func (e SysApi) GetPage(c *gin.Context) {
s := service.SysApi{}
req := dto.SysApiGetPageReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.Form).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
//数据权限检查
p := actions.GetPermissionFromContext(c)
list := make([]models.SysApi, 0)
var count int64
err = s.GetPage(&req, p, &list, &count)
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}
// Get 获取接口管理
// @Summary 获取接口管理
// @Description 获取接口管理
// @Tags 接口管理
// @Param id path string false "id"
// @Success 200 {object} response.Response{data=models.SysApi} "{"code": 200, "data": [...]}"
// @Router /api/v1/sys-api/{id} [get]
// @Security Bearer
func (e SysApi) Get(c *gin.Context) {
req := dto.SysApiGetReq{}
s := service.SysApi{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
p := actions.GetPermissionFromContext(c)
var object models.SysApi
err = s.Get(&req, p, &object).Error
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.OK(object, "查询成功")
}
// Update 修改接口管理
// @Summary 修改接口管理
// @Description 修改接口管理
// @Tags 接口管理
// @Accept application/json
// @Product application/json
// @Param data body dto.SysApiUpdateReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
// @Router /api/v1/sys-api/{id} [put]
// @Security Bearer
func (e SysApi) Update(c *gin.Context) {
req := dto.SysApiUpdateReq{}
s := service.SysApi{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
return
}
req.SetUpdateBy(user.GetUserId(c))
p := actions.GetPermissionFromContext(c)
err = s.Update(&req, p)
if err != nil {
e.Error(500, err, "更新失败")
return
}
e.OK(req.GetId(), "更新成功")
}
// DeleteSysApi 删除接口管理
// @Summary 删除接口管理
// @Description 删除接口管理
// @Tags 接口管理
// @Param data body dto.SysApiDeleteReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}"
// @Router /api/v1/sys-api [delete]
// @Security Bearer
func (e SysApi) DeleteSysApi(c *gin.Context) {
req := dto.SysApiDeleteReq{}
s := service.SysApi{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
return
}
p := actions.GetPermissionFromContext(c)
err = s.Remove(&req, p)
if err != nil {
e.Error(500, err, "删除失败")
return
}
e.OK(req.GetId(), "删除成功")
}
@@ -1,195 +0,0 @@
package sys_china_area_data
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
"go-admin/common/apis"
)
type SysChinaAreaData struct {
apis.Api
}
func (e SysChinaAreaData) GetSysChinaAreaDataList(c *gin.Context) {
e.SetContext(c)
log := e.GetLogger()
d := new(dto.SysChinaAreaDataSearch)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查询列表
err = d.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
//数据权限检查
p := actions.GetPermissionFromContext(c)
list := make([]models.SysChinaAreaData, 0)
var count int64
serviceStudent := service.SysChinaAreaData{}
serviceStudent.Log = log
serviceStudent.Orm = db
err = serviceStudent.GetSysChinaAreaDataPage(d, p, &list, &count)
if err != nil {
e.Error(http.StatusInternalServerError, err, "查询失败")
return
}
e.PageOK(list, int(count), d.GetPageIndex(), d.GetPageSize(), "查询成功")
}
func (e SysChinaAreaData) GetSysChinaAreaData(c *gin.Context) {
e.SetContext(c)
log := e.GetLogger()
control := new(dto.SysChinaAreaDataById)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查看详情
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
var object models.SysChinaAreaData
//数据权限检查
p := actions.GetPermissionFromContext(c)
serviceSysChinaAreaData := service.SysChinaAreaData{}
serviceSysChinaAreaData.Log = log
serviceSysChinaAreaData.Orm = db
err = serviceSysChinaAreaData.GetSysChinaAreaData(control, p, &object)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.OK(object, "查看成功")
}
func (e SysChinaAreaData) InsertSysChinaAreaData(c *gin.Context) {
e.SetContext(c)
log := e.GetLogger()
control := new(dto.SysChinaAreaDataControl)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//新增操作
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, err := control.Generate()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
// 设置创建人
object.SetCreateBy(user.GetUserId(c))
serviceSysChinaAreaData := service.SysChinaAreaData{}
serviceSysChinaAreaData.Orm = db
serviceSysChinaAreaData.Log = log
err = serviceSysChinaAreaData.InsertSysChinaAreaData(object)
if err != nil {
log.Error(err)
e.Error(http.StatusInternalServerError, err, "创建失败")
return
}
e.OK(object.GetId(), "创建成功")
}
func (e SysChinaAreaData) UpdateSysChinaAreaData(c *gin.Context) {
e.SetContext(c)
log := e.GetLogger()
control := new(dto.SysChinaAreaDataControl)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//更新操作
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, err := control.Generate()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
object.SetUpdateBy(user.GetUserId(c))
//数据权限检查
p := actions.GetPermissionFromContext(c)
serviceSysChinaAreaData := service.SysChinaAreaData{}
serviceSysChinaAreaData.Orm = db
serviceSysChinaAreaData.Log = log
err = serviceSysChinaAreaData.UpdateSysChinaAreaData(object, p)
if err != nil {
log.Error(err)
return
}
e.OK(object.GetId(), "更新成功")
}
func (e SysChinaAreaData) DeleteSysChinaAreaData(c *gin.Context) {
e.SetContext(c)
log := e.GetLogger()
control := new(dto.SysChinaAreaDataById)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//删除操作
err = control.Bind(c)
if err != nil {
log.Errorf("Bind error: %s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
// 设置编辑人
control.SetUpdateBy(user.GetUserId(c))
// 数据权限检查
p := actions.GetPermissionFromContext(c)
serviceSysChinaAreaData := service.SysChinaAreaData{}
serviceSysChinaAreaData.Orm = db
serviceSysChinaAreaData.Log = log
err = serviceSysChinaAreaData.RemoveSysChinaAreaData(control, p)
if err != nil {
log.Errorf("RemoveSysChinaAreaData error, %s", err)
e.Error(http.StatusInternalServerError, err, "删除失败")
return
}
e.OK(control.GetId(), "删除成功")
}
+313
View File
@@ -0,0 +1,313 @@
package apis
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
)
type SysConfig struct {
api.Api
}
// GetPage 获取配置管理列表
// @Summary 获取配置管理列表
// @Description 获取配置管理列表
// @Tags 配置管理
// @Param configName query string false "名称"
// @Param configKey query string false "key"
// @Param configType query string false "类型"
// @Param isFrontend query int false "是否前端"
// @Param pageSize query int false "页条数"
// @Param pageIndex query int false "页码"
// @Success 200 {object} response.Response{data=response.Page{list=[]models.SysApi}} "{"code": 200, "data": [...]}"
// @Router /api/v1/sys-config [get]
// @Security Bearer
func (e SysConfig) GetPage(c *gin.Context) {
s := service.SysConfig{}
req := dto.SysConfigGetPageReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.Form).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
return
}
list := make([]models.SysConfig, 0)
var count int64
err = s.GetPage(&req, &list, &count)
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}
// Get 获取配置管理
// @Summary 获取配置管理
// @Description 获取配置管理
// @Tags 配置管理
// @Param id path string false "id"
// @Success 200 {object} response.Response{data=models.SysConfig} "{"code": 200, "data": [...]}"
// @Router /api/v1/sys-config/{id} [get]
// @Security Bearer
func (e SysConfig) Get(c *gin.Context) {
req := dto.SysConfigGetReq{}
s := service.SysConfig{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
var object models.SysConfig
err = s.Get(&req, &object)
if err != nil {
e.Error(500, err, err.Error())
return
}
e.OK(object, "查询成功")
}
// Insert 创建配置管理
// @Summary 创建配置管理
// @Description 创建配置管理
// @Tags 配置管理
// @Accept application/json
// @Product application/json
// @Param data body dto.SysConfigControl true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "创建成功"}"
// @Router /api/v1/sys-config [post]
// @Security Bearer
func (e SysConfig) Insert(c *gin.Context) {
s := service.SysConfig{}
req := dto.SysConfigControl{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.SetCreateBy(user.GetUserId(c))
err = s.Insert(&req)
if err != nil {
e.Error(500, err, "创建失败")
return
}
e.OK(req.GetId(), "创建成功")
}
// Update 修改配置管理
// @Summary 修改配置管理
// @Description 修改配置管理
// @Tags 配置管理
// @Accept application/json
// @Product application/json
// @Param data body dto.SysConfigControl true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
// @Router /api/v1/sys-config/{id} [put]
// @Security Bearer
func (e SysConfig) Update(c *gin.Context) {
s := service.SysConfig{}
req := dto.SysConfigControl{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.SetUpdateBy(user.GetUserId(c))
err = s.Update(&req)
if err != nil {
e.Error(500, err, "更新失败")
return
}
e.OK(req.GetId(), "更新成功")
}
// Delete 删除配置管理
// @Summary 删除配置管理
// @Description 删除配置管理
// @Tags 配置管理
// @Param ids body []int false "ids"
// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}"
// @Router /api/v1/sys-config [delete]
// @Security Bearer
func (e SysConfig) Delete(c *gin.Context) {
s := service.SysConfig{}
req := dto.SysConfigDeleteReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.SetUpdateBy(user.GetUserId(c))
err = s.Remove(&req)
if err != nil {
e.Error(500, err, "删除失败")
return
}
e.OK(req.GetId(), "删除成功")
}
// Get2SysApp 获取系统配置信息
// @Summary 获取系统前台配置信息,主要注意这里不在验证权限
// @Description 获取系统配置信息,主要注意这里不在验证权限
// @Tags 配置管理
// @Success 200 {object} response.Response{data=map[string]string} "{"code": 200, "data": [...]}"
// @Router /api/v1/app-config [get]
func (e SysConfig) Get2SysApp(c *gin.Context) {
req := dto.SysConfigGetToSysAppReq{}
s := service.SysConfig{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.Form).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
return
}
// 控制只读前台的数据
req.IsFrontend = "1"
list := make([]models.SysConfig, 0)
err = s.GetWithKeyList(&req, &list)
if err != nil {
e.Error(500, err, "查询失败")
return
}
mp := make(map[string]string)
for i := 0; i < len(list); i++ {
key := list[i].ConfigKey
if key != "" {
mp[key] = list[i].ConfigValue
}
}
e.OK(mp, "查询成功")
}
// Get2Set 获取配置
// @Summary 获取配置
// @Description 界面操作设置配置值的获取
// @Tags 配置管理
// @Accept application/json
// @Product application/json
// @Success 200 {object} response.Response{data=map[string]interface{}} "{"code": 200, "message": "修改成功"}"
// @Router /api/v1/set-config [get]
// @Security Bearer
func (e SysConfig) Get2Set(c *gin.Context) {
s := service.SysConfig{}
req := make([]dto.GetSetSysConfigReq, 0)
err := e.MakeContext(c).
MakeOrm().
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
err = s.GetForSet(&req)
if err != nil {
e.Error(500, err, "查询失败")
return
}
m := make(map[string]interface{}, 0)
for _, v := range req {
m[v.ConfigKey] = v.ConfigValue
}
e.OK(m, "查询成功")
}
// Update2Set 设置配置
// @Summary 设置配置
// @Description 界面操作设置配置值
// @Tags 配置管理
// @Accept application/json
// @Product application/json
// @Param data body []dto.GetSetSysConfigReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
// @Router /api/v1/set-config [put]
// @Security Bearer
func (e SysConfig) Update2Set(c *gin.Context) {
s := service.SysConfig{}
req := make([]dto.GetSetSysConfigReq, 0)
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
err = s.UpdateForSet(&req)
if err != nil {
e.Error(500, err, err.Error())
return
}
e.OK("", "更新成功")
}
// GetSysConfigByKEYForService 根据Key获取SysConfig的Service
// @Summary 根据Key获取SysConfig的Service
// @Description 根据Key获取SysConfig的Service
// @Tags 配置管理
// @Param configKey path string false "configKey"
// @Success 200 {object} response.Response{data=dto.SysConfigByKeyReq} "{"code": 200, "data": [...]}"
// @Router /api/v1/sys-config/{id} [get]
// @Security Bearer
func (e SysConfig) GetSysConfigByKEYForService(c *gin.Context) {
var s = new(service.SysConfig)
var req = new(dto.SysConfigByKeyReq)
var resp = new(dto.GetSysConfigByKEYForServiceResp)
err := e.MakeContext(c).
MakeOrm().
Bind(req, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
err = s.GetWithKey(req, resp)
if err != nil {
e.Error(500, err, err.Error())
return
}
e.OK(resp, s.Msg)
}
+238
View File
@@ -0,0 +1,238 @@
package apis
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/v2/response"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
)
type SysDept struct {
api.Api
}
// GetPage
// @Summary 分页部门列表数据
// @Description 分页列表
// @Tags 部门
// @Param deptName query string false "deptName"
// @Param deptId query string false "deptId"
// @Param position query string false "position"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/dept [get]
// @Security Bearer
func (e SysDept) GetPage(c *gin.Context) {
s := service.SysDept{}
req := dto.SysDeptGetPageReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
list := make([]models.SysDept, 0)
list, err = s.SetDeptPage(&req)
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.OK(list, "查询成功")
}
// Get
// @Summary 获取部门数据
// @Description 获取JSON
// @Tags 部门
// @Param deptId path string false "deptId"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/dept/{deptId} [get]
// @Security Bearer
func (e SysDept) Get(c *gin.Context) {
s := service.SysDept{}
req := dto.SysDeptGetReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
var object models.SysDept
err = s.Get(&req, &object)
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.OK(object, "查询成功")
}
// Insert 添加部门
// @Summary 添加部门
// @Description 获取JSON
// @Tags 部门
// @Accept application/json
// @Product application/json
// @Param data body dto.SysDeptInsertReq true "data"
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/dept [post]
// @Security Bearer
func (e SysDept) Insert(c *gin.Context) {
s := service.SysDept{}
req := dto.SysDeptInsertReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
// 设置创建人
req.SetCreateBy(user.GetUserId(c))
err = s.Insert(&req)
if err != nil {
e.Error(500, err, "创建失败")
return
}
e.OK(req.GetId(), "创建成功")
}
// Update
// @Summary 修改部门
// @Description 获取JSON
// @Tags 部门
// @Accept application/json
// @Product application/json
// @Param id path int true "id"
// @Param data body dto.SysDeptUpdateReq true "body"
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/dept/{deptId} [put]
// @Security Bearer
func (e SysDept) Update(c *gin.Context) {
s := service.SysDept{}
req := dto.SysDeptUpdateReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.SetUpdateBy(user.GetUserId(c))
err = s.Update(&req)
if err != nil {
e.Error(500, err, err.Error())
return
}
e.OK(req.GetId(), "更新成功")
}
// Delete
// @Summary 删除部门
// @Description 删除数据
// @Tags 部门
// @Param data body dto.SysDeptDeleteReq true "body"
// @Success 200 {string} string "{"code": 200, "message": "删除成功"}"
// @Success 200 {string} string "{"code": -1, "message": "删除失败"}"
// @Router /api/v1/dept [delete]
// @Security Bearer
func (e SysDept) Delete(c *gin.Context) {
s := service.SysDept{}
req := dto.SysDeptDeleteReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
err = s.Remove(&req)
if err != nil {
e.Error(500, err, "删除失败")
return
}
e.OK(req.GetId(), "删除成功")
}
// Get2Tree 用户管理 左侧部门树
func (e SysDept) Get2Tree(c *gin.Context) {
s := service.SysDept{}
req := dto.SysDeptGetPageReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.Form).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
list := make([]dto.DeptLabel, 0)
list, err = s.SetDeptTree(&req)
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.OK(list, "")
}
// GetDeptTreeRoleSelect TODO: 此接口需要调整不应该将list和选中放在一起
func (e SysDept) GetDeptTreeRoleSelect(c *gin.Context) {
s := service.SysDept{}
err := e.MakeContext(c).
MakeOrm().
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
id, err := pkg.StringToInt(c.Param("roleId"))
result, err := s.SetDeptLabel()
if err != nil {
e.Error(500, err, err.Error())
return
}
menuIds := make([]int, 0)
if id != 0 {
menuIds, err = s.GetWithRoleId(id)
if err != nil {
e.Error(500, err, err.Error())
return
}
}
e.OK(gin.H{
"depts": result,
"checkedKeys": menuIds,
}, "")
}
+220
View File
@@ -0,0 +1,220 @@
package apis
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/v2/response"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
)
type SysDictData struct {
api.Api
}
// GetPage
// @Summary 字典数据列表
// @Description 获取JSON
// @Tags 字典数据
// @Param status query string false "status"
// @Param dictCode query string false "dictCode"
// @Param dictType query string false "dictType"
// @Param pageSize query int false "页条数"
// @Param pageIndex query int false "页码"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/dict/data [get]
// @Security Bearer
func (e SysDictData) GetPage(c *gin.Context) {
s := service.SysDictData{}
req := dto.SysDictDataGetPageReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.Form).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
list := make([]models.SysDictData, 0)
var count int64
err = s.GetPage(&req, &list, &count)
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}
// Get
// @Summary 通过编码获取字典数据
// @Description 获取JSON
// @Tags 字典数据
// @Param dictCode path int true "字典编码"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/dict/data/{dictCode} [get]
// @Security Bearer
func (e SysDictData) Get(c *gin.Context) {
s := service.SysDictData{}
req := dto.SysDictDataGetReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
var object models.SysDictData
err = s.Get(&req, &object)
if err != nil {
e.Logger.Warnf("Get error: %s", err.Error())
e.Error(500, err, "查询失败")
return
}
e.OK(object, "查询成功")
}
// Insert
// @Summary 添加字典数据
// @Description 获取JSON
// @Tags 字典数据
// @Accept application/json
// @Product application/json
// @Param data body dto.SysDictDataInsertReq true "data"
// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}"
// @Router /api/v1/dict/data [post]
// @Security Bearer
func (e SysDictData) Insert(c *gin.Context) {
s := service.SysDictData{}
req := dto.SysDictDataInsertReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.SetCreateBy(user.GetUserId(c))
err = s.Insert(&req)
if err != nil {
e.Error(500, err, "创建失败")
return
}
e.OK(req.GetId(), "创建成功")
}
// Update
// @Summary 修改字典数据
// @Description 获取JSON
// @Tags 字典数据
// @Accept application/json
// @Product application/json
// @Param data body dto.SysDictDataUpdateReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
// @Router /api/v1/dict/data/{dictCode} [put]
// @Security Bearer
func (e SysDictData) Update(c *gin.Context) {
s := service.SysDictData{}
req := dto.SysDictDataUpdateReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.SetUpdateBy(user.GetUserId(c))
err = s.Update(&req)
if err != nil {
e.Error(500, err, "更新失败")
return
}
e.OK(req.GetId(), "更新成功")
}
// Delete
// @Summary 删除字典数据
// @Description 删除数据
// @Tags 字典数据
// @Param dictCode body dto.SysDictDataDeleteReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}"
// @Router /api/v1/dict/data [delete]
// @Security Bearer
func (e SysDictData) Delete(c *gin.Context) {
s := service.SysDictData{}
req := dto.SysDictDataDeleteReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.SetUpdateBy(user.GetUserId(c))
err = s.Remove(&req)
if err != nil {
e.Error(500, err, "删除失败")
return
}
e.OK(req.GetId(), "删除成功")
}
// GetAll 数据字典根据key获取 业务页面使用
// @Summary 数据字典根据key获取
// @Description 数据字典根据key获取
// @Tags 字典数据
// @Param dictType query int true "dictType"
// @Success 200 {object} response.Response{data=[]dto.SysDictDataGetAllResp} "{"code": 200, "data": [...]}"
// @Router /api/v1/dict-data/option-select [get]
// @Security Bearer
func (e SysDictData) GetAll(c *gin.Context) {
s := service.SysDictData{}
req := dto.SysDictDataGetPageReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
list := make([]models.SysDictData, 0)
err = s.GetAll(&req, &list)
if err != nil {
e.Error(500, err, "查询失败")
return
}
l := make([]dto.SysDictDataGetAllResp, 0)
for _, i := range list {
d := dto.SysDictDataGetAllResp{}
e.Translate(i, &d)
l = append(l, d)
}
e.OK(l, "查询成功")
}
+210
View File
@@ -0,0 +1,210 @@
package apis
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/v2/response"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
)
type SysDictType struct {
api.Api
}
// GetPage 字典类型列表数据
// @Summary 字典类型列表数据
// @Description 获取JSON
// @Tags 字典类型
// @Param dictName query string false "dictName"
// @Param dictId query string false "dictId"
// @Param dictType query string false "dictType"
// @Param pageSize query int false "页条数"
// @Param pageIndex query int false "页码"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/dict/type [get]
// @Security Bearer
func (e SysDictType) GetPage(c *gin.Context) {
s := service.SysDictType{}
req := dto.SysDictTypeGetPageReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.Form).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
list := make([]models.SysDictType, 0)
var count int64
err = s.GetPage(&req, &list, &count)
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}
// Get 字典类型通过字典id获取
// @Summary 字典类型通过字典id获取
// @Description 获取JSON
// @Tags 字典类型
// @Param dictId path int true "字典类型编码"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/dict/type/{dictId} [get]
// @Security Bearer
func (e SysDictType) Get(c *gin.Context) {
s := service.SysDictType{}
req := dto.SysDictTypeGetReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
var object models.SysDictType
err = s.Get(&req, &object)
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.OK(object, "查询成功")
}
// Insert 字典类型创建
// @Summary 添加字典类型
// @Description 获取JSON
// @Tags 字典类型
// @Accept application/json
// @Product application/json
// @Param data body dto.SysDictTypeInsertReq true "data"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/dict/type [post]
// @Security Bearer
func (e SysDictType) Insert(c *gin.Context) {
s := service.SysDictType{}
req := dto.SysDictTypeInsertReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.SetCreateBy(user.GetUserId(c))
err = s.Insert(&req)
if err != nil {
e.Logger.Error(err)
e.Error(500, err, fmt.Sprintf(" 创建字典类型失败,详情:%s", err.Error()))
return
}
e.OK(req.GetId(), "创建成功")
}
// Update
// @Summary 修改字典类型
// @Description 获取JSON
// @Tags 字典类型
// @Accept application/json
// @Product application/json
// @Param data body dto.SysDictTypeUpdateReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/dict/type/{dictId} [put]
// @Security Bearer
func (e SysDictType) Update(c *gin.Context) {
s := service.SysDictType{}
req := dto.SysDictTypeUpdateReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Error(500, err, err.Error())
e.Logger.Error(err)
return
}
req.SetUpdateBy(user.GetUserId(c))
err = s.Update(&req)
if err != nil {
e.Logger.Error(err)
return
}
e.OK(req.GetId(), "更新成功")
}
// Delete
// @Summary 删除字典类型
// @Description 删除数据
// @Tags 字典类型
// @Param dictCode body dto.SysDictTypeDeleteReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/dict/type [delete]
// @Security Bearer
func (e SysDictType) Delete(c *gin.Context) {
s := service.SysDictType{}
req := dto.SysDictTypeDeleteReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.SetUpdateBy(user.GetUserId(c))
err = s.Remove(&req)
if err != nil {
e.Error(500, err, err.Error())
return
}
e.OK(req.GetId(), "删除成功")
}
// GetAll
// @Summary 字典类型全部数据 代码生成使用接口
// @Description 获取JSON
// @Tags 字典类型
// @Param dictName query string false "dictName"
// @Param dictId query string false "dictId"
// @Param dictType query string false "dictType"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/dict/type-option-select [get]
// @Security Bearer
func (e SysDictType) GetAll(c *gin.Context) {
s := service.SysDictType{}
req := dto.SysDictTypeGetPageReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.Form).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
list := make([]models.SysDictType, 0)
err = s.GetAll(&req, &list)
if err != nil {
e.Error(500, err, err.Error())
return
}
e.OK(list, "查询成功")
}
-198
View File
@@ -1,198 +0,0 @@
package sys_file
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/pkg"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
"go-admin/common/apis"
)
type SysFileDir struct {
apis.Api
}
func (e SysFileDir) GetSysFileDirList(c *gin.Context) {
e.Context = c
log := e.GetLogger()
search := new(dto.SysFileDirSearch)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
err = c.ShouldBind(search)
if err != nil {
log.Debugf("ShouldBind error: %s", err.Error())
}
var list *[]models.SysFileDirL
serviceStudent := service.SysFileDir{}
serviceStudent.Log = log
serviceStudent.Orm = db
list, err = serviceStudent.SetSysFileDir(search)
if err != nil {
log.Errorf("SetSysFileDir error, %s", err)
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.OK(list, "查询成功")
}
func (e SysFileDir) GetSysFileDir(c *gin.Context) {
control := new(dto.SysFileDirById)
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查看详情
err = c.ShouldBindUri(control)
if err != nil {
log.Warnf("ShouldBindUri error: %s", err.Error())
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
}
var object models.SysFileDir
serviceSysFileDir := service.SysFileDir{}
serviceSysFileDir.Log = log
serviceSysFileDir.Orm = db
err = serviceSysFileDir.GetSysFileDir(control, &object)
if err != nil {
log.Errorf("GetSysFileDir error, %s", err)
e.Error(http.StatusInternalServerError, err, "查询失败")
return
}
e.OK(object, "查看成功")
}
func (e SysFileDir) InsertSysFileDir(c *gin.Context) {
control := new(dto.SysFileDirControl)
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//新增操作
err = c.ShouldBindUri(control)
if err != nil {
log.Warnf("ShouldBindUri error: %s", err.Error())
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
err = c.ShouldBind(control)
if err != nil {
log.Warnf("ShouldBind error: %s", err.Error())
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
// 设置创建人
control.CreateBy = user.GetUserId(c)
serviceSysFileDir := service.SysFileDir{}
serviceSysFileDir.Orm = db
serviceSysFileDir.Log = log
err = serviceSysFileDir.InsertSysFileDir(control)
if err != nil {
log.Errorf("InsertSysFileDir error, %s", err)
e.Error(http.StatusInternalServerError, err, "创建失败")
return
}
e.OK(control.ID, "创建成功")
}
func (e SysFileDir) UpdateSysFileDir(c *gin.Context) {
control := new(dto.SysFileDirControl)
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
err = c.ShouldBindUri(control)
if err != nil {
log.Warnf("ShouldBindUri error: %s", err.Error())
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
}
err = c.ShouldBind(control)
if err != nil {
log.Warnf("ShouldBind error: %#v", err.Error())
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
}
// 设置创建人
control.UpdateBy = user.GetUserId(c)
//数据权限检查
p := actions.GetPermissionFromContext(c)
serviceSysFileDir := service.SysFileDir{}
serviceSysFileDir.Orm = db
serviceSysFileDir.Log = log
err = serviceSysFileDir.UpdateSysFileDir(control, p)
if err != nil {
log.Errorf("UpdateSysFileDir error, %s", err)
e.Error(http.StatusInternalServerError, err, "更新失败")
return
}
e.OK(control.ID, "更新成功")
}
func (e SysFileDir) DeleteSysFileDir(c *gin.Context) {
control := new(dto.SysFileDirById)
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
msgID := pkg.GenerateMsgIDFromContext(c)
//删除操作
err = c.ShouldBindUri(control)
if err != nil {
log.Warnf("MsgID[%s] ShouldBindUri error: %s", msgID, err.Error())
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
}
err = c.ShouldBind(control)
if err != nil {
log.Warnf("MsgID[%s] ShouldBind error: %#v", msgID, err.Error())
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
}
// 设置编辑人
control.UpdateBy = user.GetUserId(c)
// 数据权限检查
p := actions.GetPermissionFromContext(c)
serviceSysFileDir := service.SysFileDir{}
serviceSysFileDir.Orm = db
serviceSysFileDir.MsgID = msgID
err = serviceSysFileDir.RemoveSysFileDir(control, p)
if err != nil {
log.Errorf("RemoveSysFileDir error, %s", err)
e.Error(http.StatusInternalServerError, err, "删除失败")
return
}
e.OK(control.Id, "删除成功")
}
-209
View File
@@ -1,209 +0,0 @@
package sys_file
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
"go-admin/common/apis"
)
type SysFileInfo struct {
apis.Api
}
func (e SysFileInfo) GetSysFileInfoList(c *gin.Context) {
e.Context = c
log := e.GetLogger()
search := new(dto.SysFileInfoSearch)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
err = c.ShouldBind(search)
if err != nil {
log.Warnf("参数验证错误, error: %s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
//数据权限检查
p := actions.GetPermissionFromContext(c)
list := make([]models.SysFileInfo, 0)
var count int64
serviceStudent := service.SysFileInfo{}
serviceStudent.Log = log
serviceStudent.Orm = db
err = serviceStudent.GetSysFileInfoPage(search, p, &list, &count)
if err != nil {
log.Errorf("GetSysFileInfoPage error, %s", err.Error())
e.Error(http.StatusInternalServerError, err, "查询失败")
return
}
e.PageOK(list, int(count), search.PageIndex, search.PageSize, "查询成功")
}
func (e SysFileInfo) GetSysFileInfo(c *gin.Context) {
control := new(dto.SysFileInfoById)
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查看详情
err = c.ShouldBindUri(control)
if err != nil {
log.Warnf("参数验证错误, error:%s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
var object models.SysFileInfo
//数据权限检查
p := actions.GetPermissionFromContext(c)
serviceSysFileInfo := service.SysFileInfo{}
serviceSysFileInfo.Log = log
serviceSysFileInfo.Orm = db
err = serviceSysFileInfo.GetSysFileInfo(control, p, &object)
if err != nil {
log.Errorf("GetSysFileInfo error, %s", err.Error())
e.Error(http.StatusInternalServerError, err, "查询失败")
return
}
e.OK(object, "查看成功")
}
func (e SysFileInfo) InsertSysFileInfo(c *gin.Context) {
control := new(dto.SysFileInfoControl)
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//新增操作
err = c.ShouldBindUri(control)
if err != nil {
log.Warnf("参数验证错误, error: %s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
err = c.ShouldBind(control)
if err != nil {
log.Warnf("参数验证错误, error: %s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
// 设置创建人
control.CreateBy = user.GetUserId(c)
serviceSysFileInfo := service.SysFileInfo{}
serviceSysFileInfo.Orm = db
serviceSysFileInfo.Log = log
err = serviceSysFileInfo.InsertSysFileInfo(control)
if err != nil {
log.Errorf("InsertSysFileInfo error: %s", err)
e.Error(http.StatusInternalServerError, err, "创建失败")
return
}
e.OK(control.ID, "创建成功")
}
func (e SysFileInfo) UpdateSysFileInfo(c *gin.Context) {
control := new(dto.SysFileInfoControl)
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
err = c.ShouldBindUri(control)
if err != nil {
log.Warnf("参数验证错误, error: %s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
err = c.ShouldBind(control)
if err != nil {
log.Warnf("参数验证错误, error:%s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
// 设置创建人
control.UpdateBy = user.GetUserId(c)
//数据权限检查
p := actions.GetPermissionFromContext(c)
serviceSysFileInfo := service.SysFileInfo{}
serviceSysFileInfo.Orm = db
serviceSysFileInfo.Log = log
err = serviceSysFileInfo.UpdateSysFileInfo(control, p)
if err != nil {
log.Errorf("UpdateSysFileInfo error: %s", err)
e.Error(http.StatusInternalServerError, err, "创建失败")
return
}
e.OK(control.ID, "更新成功")
}
func (e SysFileInfo) DeleteSysFileInfo(c *gin.Context) {
control := new(dto.SysFileInfoById)
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//删除操作
err = c.ShouldBindUri(control)
if err != nil {
log.Warnf("参数验证错误, error: %s", err)
e.Error(422, err, "参数验证失败")
return
}
err = c.ShouldBind(control)
if err != nil {
log.Warnf("参数验证错误, error: %s", err)
e.Error(422, err, "参数验证失败")
return
}
// 设置编辑人
control.UpdateBy = user.GetUserId(c)
// 数据权限检查
p := actions.GetPermissionFromContext(c)
serviceSysFileInfo := service.SysFileInfo{}
serviceSysFileInfo.Orm = db
serviceSysFileInfo.Log = log
err = serviceSysFileInfo.RemoveSysFileInfo(control, p)
if err != nil {
log.Errorf("RemoveSysFileInfo error: %s", err)
e.Error(http.StatusInternalServerError, err, "创建失败")
return
}
e.OK(control.Id, "删除成功")
}
+110
View File
@@ -0,0 +1,110 @@
package apis
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
)
type SysLoginLog struct {
api.Api
}
// GetPage 登录日志列表
// @Summary 登录日志列表
// @Description 获取JSON
// @Tags 登录日志
// @Param username query string false "用户名"
// @Param ipaddr query string false "ip地址"
// @Param loginLocation query string false "归属地"
// @Param status query string false "状态"
// @Param beginTime query string false "开始时间"
// @Param endTime query string false "结束时间"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/sys-login-log [get]
// @Security Bearer
func (e SysLoginLog) GetPage(c *gin.Context) {
s := service.SysLoginLog{}
req := dto.SysLoginLogGetPageReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.Form).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
list := make([]models.SysLoginLog, 0)
var count int64
err = s.GetPage(&req, &list, &count)
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}
// Get 登录日志通过id获取
// @Summary 登录日志通过id获取
// @Description 获取JSON
// @Tags 登录日志
// @Param id path string false "id"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/sys-login-log/{id} [get]
// @Security Bearer
func (e SysLoginLog) Get(c *gin.Context) {
s := service.SysLoginLog{}
req := dto.SysLoginLogGetReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
var object models.SysLoginLog
err = s.Get(&req, &object)
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.OK(object, "查询成功")
}
// Delete 登录日志删除
// @Summary 登录日志删除
// @Description 登录日志删除
// @Tags 登录日志
// @Param data body dto.SysLoginLogDeleteReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/sys-login-log [delete]
// @Security Bearer
func (e SysLoginLog) Delete(c *gin.Context) {
s := service.SysLoginLog{}
req := dto.SysLoginLogDeleteReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
err = s.Remove(&req)
if err != nil {
e.Error(500, err, "删除失败")
return
}
e.OK(req.GetId(), "删除成功")
}
+249
View File
@@ -0,0 +1,249 @@
package apis
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
)
type SysMenu struct {
api.Api
}
// GetPage Menu列表数据
// @Summary Menu列表数据
// @Description 获取JSON
// @Tags 菜单
// @Param menuName query string false "menuName"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/menu [get]
// @Security Bearer
func (e SysMenu) GetPage(c *gin.Context) {
s := service.SysMenu{}
req := dto.SysMenuGetPageReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.Form).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
var list = make([]models.SysMenu, 0)
err = s.GetPage(&req, &list).Error
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.OK(list, "查询成功")
}
// Get 获取菜单详情
// @Summary Menu详情数据
// @Description 获取JSON
// @Tags 菜单
// @Param id path string false "id"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/menu/{id} [get]
// @Security Bearer
func (e SysMenu) Get(c *gin.Context) {
req := dto.SysMenuGetReq{}
s := new(service.SysMenu)
err := e.MakeContext(c).
MakeOrm().
Bind(&req, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
var object = models.SysMenu{}
err = s.Get(&req, &object).Error
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.OK(object, "查询成功")
}
// Insert 创建菜单
// @Summary 创建菜单
// @Description 获取JSON
// @Tags 菜单
// @Accept application/json
// @Product application/json
// @Param data body dto.SysMenuInsertReq true "data"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/menu [post]
// @Security Bearer
func (e SysMenu) Insert(c *gin.Context) {
req := dto.SysMenuInsertReq{}
s := new(service.SysMenu)
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
// 设置创建人
req.SetCreateBy(user.GetUserId(c))
err = s.Insert(&req).Error
if err != nil {
e.Error(500, err, "创建失败")
return
}
e.OK(req.GetId(), "创建成功")
}
// Update 修改菜单
// @Summary 修改菜单
// @Description 获取JSON
// @Tags 菜单
// @Accept application/json
// @Product application/json
// @Param id path int true "id"
// @Param data body dto.SysMenuUpdateReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/menu/{id} [put]
// @Security Bearer
func (e SysMenu) Update(c *gin.Context) {
req := dto.SysMenuUpdateReq{}
s := new(service.SysMenu)
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.SetUpdateBy(user.GetUserId(c))
err = s.Update(&req).Error
if err != nil {
e.Error(500, err, "更新失败")
return
}
e.OK(req.GetId(), "更新成功")
}
// Delete 删除菜单
// @Summary 删除菜单
// @Description 删除数据
// @Tags 菜单
// @Param data body dto.SysMenuDeleteReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/menu [delete]
// @Security Bearer
func (e SysMenu) Delete(c *gin.Context) {
control := new(dto.SysMenuDeleteReq)
s := new(service.SysMenu)
err := e.MakeContext(c).
MakeOrm().
Bind(control, binding.JSON).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
err = s.Remove(control).Error
if err != nil {
e.Logger.Errorf("RemoveSysMenu error, %s", err)
e.Error(500, err, "删除失败")
return
}
e.OK(control.GetId(), "删除成功")
}
// GetMenuRole 根据登录角色名称获取菜单列表数据(左菜单使用)
// @Summary 根据登录角色名称获取菜单列表数据(左菜单使用)
// @Description 获取JSON
// @Tags 菜单
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/menurole [get]
// @Security Bearer
func (e SysMenu) GetMenuRole(c *gin.Context) {
s := new(service.SysMenu)
err := e.MakeContext(c).
MakeOrm().
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
result, err := s.SetMenuRole(user.GetRoleName(c))
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.OK(result, "")
}
// GetMenuTreeSelect 根据角色ID查询菜单下拉树结构
// @Summary 角色修改使用的菜单列表
// @Description 获取JSON
// @Tags 菜单
// @Accept application/json
// @Product application/json
// @Param roleId path int true "roleId"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/menuTreeselect/{roleId} [get]
// @Security Bearer
func (e SysMenu) GetMenuTreeSelect(c *gin.Context) {
m := service.SysMenu{}
r := service.SysRole{}
req := dto.SelectRole{}
err := e.MakeContext(c).
MakeOrm().
MakeService(&m.Service).
MakeService(&r.Service).
Bind(&req, nil).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
result, err := m.SetLabel()
if err != nil {
e.Error(500, err, "查询失败")
return
}
menuIds := make([]int, 0)
if req.RoleId != 0 {
menuIds, err = r.GetRoleMenuId(req.RoleId)
if err != nil {
e.Error(500, err, "")
return
}
}
e.OK(gin.H{
"menus": result,
"checkedKeys": menuIds,
}, "获取成功")
}
+118
View File
@@ -0,0 +1,118 @@
package apis
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
)
type SysOperaLog struct {
api.Api
}
// GetPage 操作日志列表
// @Summary 操作日志列表
// @Description 获取JSON
// @Tags 操作日志
// @Param title query string false "title"
// @Param method query string false "method"
// @Param requestMethod query string false "requestMethod"
// @Param operUrl query string false "operUrl"
// @Param operIp query string false "operIp"
// @Param status query string false "status"
// @Param beginTime query string false "beginTime"
// @Param endTime query string false "endTime"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/sys-opera-log [get]
// @Security Bearer
func (e SysOperaLog) GetPage(c *gin.Context) {
s := service.SysOperaLog{}
req := new(dto.SysOperaLogGetPageReq)
err := e.MakeContext(c).
MakeOrm().
Bind(req, binding.Form).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
list := make([]models.SysOperaLog, 0)
var count int64
err = s.GetPage(req, &list, &count)
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}
// Get 操作日志通过id获取
// @Summary 操作日志通过id获取
// @Description 获取JSON
// @Tags 操作日志
// @Param id path string false "id"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/sys-opera-log/{id} [get]
// @Security Bearer
func (e SysOperaLog) Get(c *gin.Context) {
s := new(service.SysOperaLog)
req := dto.SysOperaLogGetReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
var object models.SysOperaLog
err = s.Get(&req, &object)
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.OK(object, "查询成功")
}
// Delete 操作日志删除
// DeleteSysMenu 操作日志删除
// @Summary 删除操作日志
// @Description 删除数据
// @Tags 操作日志
// @Param data body dto.SysOperaLogDeleteReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/sys-opera-log [delete]
// @Security Bearer
func (e SysOperaLog) Delete(c *gin.Context) {
s := new(service.SysOperaLog)
req := dto.SysOperaLogDeleteReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
err = s.Remove(&req)
if err != nil {
e.Logger.Error(err)
e.Error(500, err, fmt.Sprintf("删除失败!错误详情:%s", err.Error()))
return
}
e.OK(req.GetId(), "删除成功")
}
+184
View File
@@ -0,0 +1,184 @@
package apis
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/v2/response"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
)
type SysPost struct {
api.Api
}
// GetPage
// @Summary 岗位列表数据
// @Description 获取JSON
// @Tags 岗位
// @Param postName query string false "postName"
// @Param postCode query string false "postCode"
// @Param postId query string false "postId"
// @Param status query string false "status"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/post [get]
// @Security Bearer
func (e SysPost) GetPage(c *gin.Context) {
s := service.SysPost{}
req := dto.SysPostPageReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.Form).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
list := make([]models.SysPost, 0)
var count int64
err = s.GetPage(&req, &list, &count)
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}
// Get
// @Summary 获取岗位信息
// @Description 获取JSON
// @Tags 岗位
// @Param id path int true "编码"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/post/{postId} [get]
// @Security Bearer
func (e SysPost) Get(c *gin.Context) {
s := service.SysPost{}
req := dto.SysPostGetReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
var object models.SysPost
err = s.Get(&req, &object)
if err != nil {
e.Error(500, err, fmt.Sprintf("岗位信息获取失败!错误详情:%s", err.Error()))
return
}
e.OK(object, "查询成功")
}
// Insert
// @Summary 添加岗位
// @Description 获取JSON
// @Tags 岗位
// @Accept application/json
// @Product application/json
// @Param data body dto.SysPostInsertReq true "data"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/post [post]
// @Security Bearer
func (e SysPost) Insert(c *gin.Context) {
s := service.SysPost{}
req := dto.SysPostInsertReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.SetCreateBy(user.GetUserId(c))
err = s.Insert(&req)
if err != nil {
e.Error(500, err, fmt.Sprintf("新建岗位失败!错误详情:%s", err.Error()))
return
}
e.OK(req.GetId(), "创建成功")
}
// Update
// @Summary 修改岗位
// @Description 获取JSON
// @Tags 岗位
// @Accept application/json
// @Product application/json
// @Param data body dto.SysPostUpdateReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/post/{id} [put]
// @Security Bearer
func (e SysPost) Update(c *gin.Context) {
s := service.SysPost{}
req := dto.SysPostUpdateReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.SetUpdateBy(user.GetUserId(c))
err = s.Update(&req)
if err != nil {
e.Error(500, err, fmt.Sprintf("岗位更新失败!错误详情:%s", err.Error()))
return
}
e.OK(req.GetId(), "更新成功")
}
// Delete
// @Summary 删除岗位
// @Description 删除数据
// @Tags 岗位
// @Param id body dto.SysPostDeleteReq true "请求参数"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/post [delete]
// @Security Bearer
func (e SysPost) Delete(c *gin.Context) {
s := service.SysPost{}
req := dto.SysPostDeleteReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.SetUpdateBy(user.GetUserId(c))
err = s.Remove(&req)
if err != nil {
e.Error(500, err, fmt.Sprintf("岗位删除失败!错误详情:%s", err.Error()))
return
}
e.OK(req.GetId(), "删除成功")
}
+284
View File
@@ -0,0 +1,284 @@
package apis
import (
"fmt"
"go-admin/common/global"
"net/http"
"github.com/gin-gonic/gin/binding"
"github.com/go-admin-team/go-admin-core/v2/sdk"
"go-admin/app/admin/models"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/v2/response"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
)
type SysRole struct {
api.Api
}
// GetPage
// @Summary 角色列表数据
// @Description Get JSON
// @Tags 角色/Role
// @Param roleName query string false "roleName"
// @Param status query string false "status"
// @Param roleKey query string false "roleKey"
// @Param pageSize query int false "页条数"
// @Param pageIndex query int false "页码"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/role [get]
// @Security Bearer
func (e SysRole) GetPage(c *gin.Context) {
s := service.SysRole{}
req := dto.SysRoleGetPageReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.Form).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
list := make([]models.SysRole, 0)
var count int64
err = s.GetPage(&req, &list, &count)
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}
// Get
// @Summary 获取Role数据
// @Description 获取JSON
// @Tags 角色/Role
// @Param roleId path string false "roleId"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/role/{id} [get]
// @Security Bearer
func (e SysRole) Get(c *gin.Context) {
s := service.SysRole{}
req := dto.SysRoleGetReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, fmt.Sprintf(" %s ", err.Error()))
return
}
var object models.SysRole
err = s.Get(&req, &object)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.OK(object, "查询成功")
}
// Insert
// @Summary 创建角色
// @Description 获取JSON
// @Tags 角色/Role
// @Accept application/json
// @Product application/json
// @Param data body dto.SysRoleInsertReq true "data"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/role [post]
// @Security Bearer
func (e SysRole) Insert(c *gin.Context) {
s := service.SysRole{}
req := dto.SysRoleInsertReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
// 设置创建人
req.CreateBy = user.GetUserId(c)
if req.Status == "" {
req.Status = "2"
}
cb := sdk.Runtime.GetCasbinByTenant(c.Request.Host)
err = s.Insert(&req, cb)
if err != nil {
e.Logger.Error(err)
e.Error(500, err, "创建失败,"+err.Error())
return
}
_, err = global.LoadPolicy(c)
if err != nil {
e.Logger.Error(err)
e.Error(500, err, "创建失败,"+err.Error())
return
}
e.OK(req.GetId(), "创建成功")
}
// Update 修改用户角色
// @Summary 修改用户角色
// @Description 获取JSON
// @Tags 角色/Role
// @Accept application/json
// @Product application/json
// @Param data body dto.SysRoleUpdateReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/role/{id} [put]
// @Security Bearer
func (e SysRole) Update(c *gin.Context) {
s := service.SysRole{}
req := dto.SysRoleUpdateReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, nil, binding.JSON).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
cb := sdk.Runtime.GetCasbinByTenant(c.Request.Host)
req.SetUpdateBy(user.GetUserId(c))
err = s.Update(&req, cb)
if err != nil {
e.Logger.Error(err)
return
}
_, err = global.LoadPolicy(c)
if err != nil {
e.Logger.Error(err)
e.Error(500, err, "更新失败,"+err.Error())
return
}
e.OK(req.GetId(), "更新成功")
}
// Delete
// @Summary 删除用户角色
// @Description 删除数据
// @Tags 角色/Role
// @Param data body dto.SysRoleDeleteReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/role [delete]
// @Security Bearer
func (e SysRole) Delete(c *gin.Context) {
s := new(service.SysRole)
req := dto.SysRoleDeleteReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, fmt.Sprintf("删除角色 %v 失败,\r\n失败信息 %s", req.Ids, err.Error()))
return
}
cb := sdk.Runtime.GetCasbinByTenant(c.Request.Host)
err = s.Remove(&req, cb)
if err != nil {
e.Logger.Error(err)
e.Error(500, err, "")
return
}
e.OK(req.GetId(), fmt.Sprintf("删除角色角色 %v 状态成功!", req.GetId()))
}
// Update2Status 修改用户角色状态
// @Summary 修改用户角色
// @Description 获取JSON
// @Tags 角色/Role
// @Accept application/json
// @Product application/json
// @Param data body dto.UpdateStatusReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/role-status/{id} [put]
// @Security Bearer
func (e SysRole) Update2Status(c *gin.Context) {
s := service.SysRole{}
req := dto.UpdateStatusReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, fmt.Sprintf("更新角色状态失败,失败原因:%s ", err.Error()))
return
}
req.SetUpdateBy(user.GetUserId(c))
err = s.UpdateStatus(&req)
if err != nil {
e.Error(500, err, fmt.Sprintf("更新角色状态失败,失败原因:%s ", err.Error()))
return
}
e.OK(req.GetId(), fmt.Sprintf("更新角色 %v 状态成功!", req.GetId()))
}
// Update2DataScope 更新角色数据权限
// @Summary 更新角色数据权限
// @Description 获取JSON
// @Tags 角色/Role
// @Accept application/json
// @Product application/json
// @Param data body dto.RoleDataScopeReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/role-status/{id} [put]
// @Security Bearer
func (e SysRole) Update2DataScope(c *gin.Context) {
s := service.SysRole{}
req := dto.RoleDataScopeReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
data := &models.SysRole{
RoleId: req.RoleId,
DataScope: req.DataScope,
DeptIds: req.DeptIds,
}
data.UpdateBy = user.GetUserId(c)
err = s.UpdateDataScope(&req).Error
if err != nil {
e.Error(500, err, fmt.Sprintf("更新角色数据权限失败!错误详情:%s", err.Error()))
return
}
e.OK(nil, "操作成功")
}
+489
View File
@@ -0,0 +1,489 @@
package apis
import (
"errors"
"github.com/gin-gonic/gin/binding"
"go-admin/app/admin/models"
"golang.org/x/crypto/bcrypt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/v2/response"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
"github.com/google/uuid"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
"go-admin/common/middleware"
)
type SysUser struct {
api.Api
}
// GetPage
// @Summary 列表用户信息数据
// @Description 获取JSON
// @Tags 用户
// @Param username query string false "username"
// @Success 200 {string} {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/sys-user [get]
// @Security Bearer
func (e SysUser) GetPage(c *gin.Context) {
s := service.SysUser{}
req := dto.SysUserGetPageReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
//数据权限检查
p := actions.GetPermissionFromContext(c)
list := make([]models.SysUser, 0)
var count int64
err = s.GetPage(&req, p, &list, &count)
if err != nil {
e.Error(500, err, "查询失败")
return
}
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}
// Get
// @Summary 获取用户
// @Description 获取JSON
// @Tags 用户
// @Param userId path int true "用户编码"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/sys-user/{userId} [get]
// @Security Bearer
func (e SysUser) Get(c *gin.Context) {
s := service.SysUser{}
req := dto.SysUserById{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
var object models.SysUser
//数据权限检查
p := actions.GetPermissionFromContext(c)
err = s.Get(&req, p, &object)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.OK(object, "查询成功")
}
// Insert
// @Summary 创建用户
// @Description 获取JSON
// @Tags 用户
// @Accept application/json
// @Product application/json
// @Param data body dto.SysUserInsertReq true "用户数据"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/sys-user [post]
// @Security Bearer
func (e SysUser) Insert(c *gin.Context) {
s := service.SysUser{}
req := dto.SysUserInsertReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
// 设置创建人
req.SetCreateBy(user.GetUserId(c))
err = s.Insert(&req)
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
e.OK(req.GetId(), "创建成功")
}
// Update
// @Summary 修改用户数据
// @Description 获取JSON
// @Tags 用户
// @Accept application/json
// @Product application/json
// @Param data body dto.SysUserUpdateReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/sys-user/{userId} [put]
// @Security Bearer
func (e SysUser) Update(c *gin.Context) {
s := service.SysUser{}
req := dto.SysUserUpdateReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
callerId := user.GetUserId(c)
// This route is in CasbinExclude so the personal-center screen can edit
// the caller's own record without a policy grant (see settings.go). That
// exclusion covers the whole route, not just the caller's own record, and
// the request carries the target userId in the body - so without this
// check here, any authenticated caller could edit any other user, up to
// and including their roleId. When the target is someone else, ask Casbin
// directly for the permission AuthCheckRole skipped.
if req.UserId != callerId {
allowed, err := middleware.EnforceRoleFor(c, c.Request.URL.Path, c.Request.Method)
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
if !allowed {
e.Error(http.StatusForbidden, errors.New("无权更新其他用户数据"), "对不起,您没有该接口访问权限,请联系管理员")
return
}
}
req.SetUpdateBy(callerId)
//数据权限检查
p := actions.GetPermissionFromContext(c)
err = s.Update(&req, p, callerId)
if err != nil {
e.Logger.Error(err)
return
}
e.OK(req.GetId(), "更新成功")
}
// Delete
// @Summary 删除用户数据
// @Description 删除数据
// @Tags 用户
// @Param userId path int true "userId"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/sys-user/{userId} [delete]
// @Security Bearer
func (e SysUser) Delete(c *gin.Context) {
s := service.SysUser{}
req := dto.SysUserById{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
// 设置编辑人
req.SetUpdateBy(user.GetUserId(c))
// 数据权限检查
p := actions.GetPermissionFromContext(c)
err = s.Remove(&req, p)
if err != nil {
e.Logger.Error(err)
return
}
e.OK(req.GetId(), "删除成功")
}
// InsetAvatar
// @Summary 修改头像
// @Description 获取JSON
// @Tags 个人中心
// @Accept multipart/form-data
// @Param file formData file true "file"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/user/avatar [post]
// @Security Bearer
func (e SysUser) InsetAvatar(c *gin.Context) {
s := service.SysUser{}
req := dto.UpdateSysUserAvatarReq{}
err := e.MakeContext(c).
MakeOrm().
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
// 数据权限检查
p := actions.GetPermissionFromContext(c)
form, _ := c.MultipartForm()
files := form.File["upload[]"]
guid := uuid.New().String()
filPath := "static/uploadfile/" + guid + ".jpg"
for _, file := range files {
e.Logger.Debugf("upload avatar file: %s", file.Filename)
// 上传文件至指定目录
err = c.SaveUploadedFile(file, filPath)
if err != nil {
e.Logger.Errorf("save file error, %s", err.Error())
e.Error(500, err, "")
return
}
}
req.UserId = p.UserId
req.Avatar = "/" + filPath
err = s.UpdateAvatar(&req, p)
if err != nil {
e.Logger.Error(err)
return
}
e.OK(filPath, "修改成功")
}
// UpdateStatus 修改用户状态
// @Summary 修改用户状态
// @Description 获取JSON
// @Tags 用户
// @Accept application/json
// @Product application/json
// @Param data body dto.UpdateSysUserStatusReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/user/status [put]
// @Security Bearer
func (e SysUser) UpdateStatus(c *gin.Context) {
s := service.SysUser{}
req := dto.UpdateSysUserStatusReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON, nil).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.SetUpdateBy(user.GetUserId(c))
//数据权限检查
p := actions.GetPermissionFromContext(c)
err = s.UpdateStatus(&req, p)
if err != nil {
e.Logger.Error(err)
return
}
e.OK(req.GetId(), "更新成功")
}
// ResetPwd 重置用户密码
// @Summary 重置用户密码
// @Description 获取JSON
// @Tags 用户
// @Accept application/json
// @Product application/json
// @Param data body dto.ResetSysUserPwdReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/user/pwd/reset [put]
// @Security Bearer
func (e SysUser) ResetPwd(c *gin.Context) {
s := service.SysUser{}
req := dto.ResetSysUserPwdReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.JSON).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.SetUpdateBy(user.GetUserId(c))
//数据权限检查
p := actions.GetPermissionFromContext(c)
err = s.ResetPwd(&req, p)
if err != nil {
e.Logger.Error(err)
return
}
e.OK(req.GetId(), "更新成功")
}
// UpdatePwd
// @Summary 修改密码
// @Description 获取JSON
// @Tags 用户
// @Accept application/json
// @Product application/json
// @Param data body dto.PassWord true "body"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/user/pwd/set [put]
// @Security Bearer
func (e SysUser) UpdatePwd(c *gin.Context) {
s := service.SysUser{}
req := dto.PassWord{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
// 数据权限检查
p := actions.GetPermissionFromContext(c)
var hash []byte
if hash, err = bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost); err != nil {
req.NewPassword = string(hash)
}
err = s.UpdatePwd(user.GetUserId(c), req.OldPassword, req.NewPassword, p)
if err != nil {
e.Logger.Error(err)
e.Error(http.StatusForbidden, err, "密码修改失败")
return
}
e.OK(nil, "密码修改成功")
}
// GetProfile
// @Summary 获取个人中心用户
// @Description 获取JSON
// @Tags 个人中心
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/user/profile [get]
// @Security Bearer
func (e SysUser) GetProfile(c *gin.Context) {
s := service.SysUser{}
req := dto.SysUserById{}
err := e.MakeContext(c).
MakeOrm().
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.Id = user.GetUserId(c)
sysUser := models.SysUser{}
roles := make([]models.SysRole, 0)
posts := make([]models.SysPost, 0)
err = s.GetProfile(&req, &sysUser, &roles, &posts)
if err != nil {
e.Logger.Errorf("get user profile error, %s", err.Error())
e.Error(500, err, "获取用户信息失败")
return
}
e.OK(gin.H{
"user": sysUser,
"roles": roles,
"posts": posts,
}, "查询成功")
}
// GetInfo
// @Summary 获取个人信息
// @Description 获取JSON
// @Tags 个人中心
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/getinfo [get]
// @Security Bearer
func (e SysUser) GetInfo(c *gin.Context) {
req := dto.SysUserById{}
s := service.SysUser{}
r := service.SysRole{}
err := e.MakeContext(c).
MakeOrm().
MakeService(&r.Service).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
var roles = make([]string, 1)
roles[0] = user.GetRoleName(c)
var permissions = make([]string, 1)
permissions[0] = "*:*:*"
var buttons = make([]string, 1)
buttons[0] = "*:*:*"
var mp = make(map[string]interface{})
mp["roles"] = roles
if user.GetRoleName(c) == "admin" || user.GetRoleName(c) == "系统管理员" {
mp["permissions"] = permissions
mp["buttons"] = buttons
} else {
list, _ := r.GetById(user.GetRoleId(c))
mp["permissions"] = list
mp["buttons"] = list
}
sysUser := models.SysUser{}
req.Id = user.GetUserId(c)
// Unscoped on purpose: the id is the caller's own, taken from the token.
// This used to go through Get with whatever GetPermissionFromContext
// returned - and this route installs no PermissionAction, so that was the
// zero value. An unset scope is not a recognised one, so once unknown
// scopes started failing closed rather than silently matching everything,
// every login on a deployment with enabledp: true ended here with a 401
// and the browser went straight back to the login page.
err = s.GetSelf(&req, &sysUser)
if err != nil {
e.Error(http.StatusUnauthorized, err, "登录失败")
return
}
mp["introduction"] = " am a super administrator"
mp["avatar"] = "https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif"
if sysUser.Avatar != "" {
mp["avatar"] = sysUser.Avatar
}
mp["userName"] = sysUser.Username
mp["userId"] = sysUser.UserId
mp["deptId"] = sysUser.DeptId
mp["name"] = sysUser.NickName
mp["code"] = 200
e.OK(mp, "")
}
-472
View File
@@ -1,472 +0,0 @@
package sys_user
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
"github.com/google/uuid"
"go-admin/app/admin/models/system"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
"go-admin/common/apis"
common "go-admin/common/models"
)
type SysUser struct {
apis.Api
}
// @Summary 列表用户信息数据
// @Description 获取JSON
// @Tags 用户
// @Param username query string false "username"
// @Success 200 {string} string "{"code": 200, "data": [...]}"
// @Success 200 {string} string "{"code": -1, "message": "抱歉未找到相关信息"}"
// @Router /api/v1/sysUser [get]
// @Security Bearer
func (e SysUser) GetSysUserList(c *gin.Context) {
e.Context = c
log := e.GetLogger()
d := new(dto.SysUserSearch)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
req := d.Generate()
//查询列表
err = req.Bind(c)
if err != nil {
log.Warnf("Bind error: %s", err.Error())
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
//数据权限检查
p := actions.GetPermissionFromContext(c)
list := make([]system.SysUser, 0)
var count int64
serviceStudent := service.SysUser{}
serviceStudent.Log = log
serviceStudent.Orm = db
err = serviceStudent.GetSysUserPage(req, p, &list, &count)
if err != nil {
e.Error(http.StatusInternalServerError, err, "查询失败")
return
}
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}
// @Summary 获取用户
// @Description 获取JSON
// @Tags 用户
// @Param userId path int true "用户编码"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/sysUser/{userId} [get]
// @Security Bearer
func (e SysUser) GetSysUser(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysUserById)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查看详情
req := control.Generate()
err = req.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
var object system.SysUser
//数据权限检查
p := actions.GetPermissionFromContext(c)
serviceSysUser := service.SysUser{}
serviceSysUser.Log = log
serviceSysUser.Orm = db
err = serviceSysUser.GetSysUser(req, p, &object)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.OK(object, "查看成功")
}
// @Summary 创建用户
// @Description 获取JSON
// @Tags 用户
// @Accept application/json
// @Product application/json
// @Param data body dto.SysUserControl true "用户数据"
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/sysUser [post]
func (e SysUser) InsertSysUser(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysUserControl)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//新增操作
req := control.Generate()
err = req.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
var object common.ActiveRecord
object, err = req.GenerateM()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
// 设置创建人
object.SetCreateBy(user.GetUserId(c))
serviceSysUser := service.SysUser{}
serviceSysUser.Orm = db
serviceSysUser.Log = log
err = serviceSysUser.InsertSysUser(object)
if err != nil {
log.Error(err)
e.Error(http.StatusInternalServerError, err, "创建失败")
return
}
e.OK(object.GetId(), "创建成功")
}
// @Summary 修改用户数据
// @Description 获取JSON
// @Tags 用户
// @Accept application/json
// @Product application/json
// @Param data body dto.SysUserControl true "body"
// @Success 200 {string} string "{"code": 200, "message": "修改成功"}"
// @Success 200 {string} string "{"code": -1, "message": "修改失败"}"
// @Router /api/v1/sysuser/{userId} [put]
func (e SysUser) UpdateSysUser(c *gin.Context) {
control := new(dto.SysUserControl)
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
req := control.Generate()
//更新操作
err = req.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
var object common.ActiveRecord
object, err = req.GenerateM()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
object.SetUpdateBy(user.GetUserId(c))
//数据权限检查
p := actions.GetPermissionFromContext(c)
serviceSysUser := service.SysUser{}
serviceSysUser.Orm = db
serviceSysUser.Log = log
err = serviceSysUser.UpdateSysUser(object, p)
if err != nil {
log.Error(err)
return
}
e.OK(object.GetId(), "更新成功")
}
// @Summary 删除用户数据
// @Description 删除数据
// @Tags 用户
// @Param userId path int true "userId"
// @Success 200 {string} string "{"code": 200, "message": "删除成功"}"
// @Success 200 {string} string "{"code": -1, "message": "删除失败"}"
// @Router /api/v1/sysuser/{userId} [delete]
func (e SysUser) DeleteSysUser(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysUserById)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//删除操作
req := control.Generate()
err = req.Bind(c)
if err != nil {
log.Errorf("Bind error: %s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
var object common.ActiveRecord
object, err = req.GenerateM()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
// 设置编辑人
object.SetUpdateBy(user.GetUserId(c))
// 数据权限检查
p := actions.GetPermissionFromContext(c)
serviceSysUser := service.SysUser{}
serviceSysUser.Orm = db
serviceSysUser.Log = log
err = serviceSysUser.RemoveSysUser(req, object, p)
if err != nil {
log.Error(err)
return
}
e.OK(object.GetId(), "删除成功")
}
// @Summary 修改头像
// @Description 获取JSON
// @Tags 用户
// @Accept multipart/form-data
// @Param file formData file true "file"
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/user/avatar [post]
func (e SysUser) InsetSysUserAvatar(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
// 数据权限检查
p := actions.GetPermissionFromContext(c)
form, _ := c.MultipartForm()
files := form.File["upload[]"]
guid := uuid.New().String()
filPath := "static/uploadfile/" + guid + ".jpg"
for _, file := range files {
log.Debugf("upload avatar file: %s", file.Filename)
// 上传文件至指定目录
err = c.SaveUploadedFile(file, filPath)
if err != nil {
log.Errorf("save file error, %s", err.Error())
e.Error(http.StatusInternalServerError, err, "")
return
}
}
object := &system.SysUser{
UserId: p.UserId,
Avatar: "/" + filPath,
}
serviceSysUser := service.SysUser{}
serviceSysUser.Orm = db
serviceSysUser.Log = log
err = serviceSysUser.UpdateSysUser(object, p)
if err != nil {
log.Error(err)
return
}
e.OK(filPath, "修改成功")
}
// @Summary 重置密码
// @Description 获取JSON
// @Tags 用户
// @Accept application/json
// @Product application/json
// @Param data body dto.PassWord true "body"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/user/pwd [post]
// @Security Bearer
func (e SysUser) SysUserUpdatePwd(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
var pwd dto.PassWord
err = c.Bind(&pwd)
if err != nil {
log.Errorf("Bind error: %s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
// 数据权限检查
p := actions.GetPermissionFromContext(c)
serviceSysUser := service.SysUser{}
serviceSysUser.Orm = db
serviceSysUser.Log = log
err = serviceSysUser.UpdateSysUserPwd(user.GetUserId(c), pwd.OldPassword, pwd.NewPassword, p)
if err != nil {
log.Error(err)
e.Error(http.StatusForbidden, err, "密码修改失败")
return
}
e.OK(nil, "密码修改成功")
}
// @Summary 获取个人中心用户
// @Description 获取JSON
// @Tags 个人中心
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/user/profile [get]
// @Security Bearer
func (e SysUser) GetSysUserProfile(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
id := user.GetUserId(c)
serviceSysUser := service.SysUser{}
serviceSysUser.Log = log
serviceSysUser.Orm = db
user := new(system.SysUser)
roles := make([]system.SysRole, 0)
posts := make([]system.SysPost, 0)
err = serviceSysUser.GetSysUserProfile(id, user, &roles, &posts)
if err != nil {
log.Errorf("get user profile error, %s", err.Error())
e.Error(http.StatusInternalServerError, err, "获取用户信息失败")
return
}
e.OK(gin.H{
"user": user,
"roles": roles,
"posts": posts,
}, "查询成功")
//var SysUser models.SysUser
//userId := tools.GetUserIdStr(c)
//SysUser.UserId, _ = tools.StringToInt(userId)
//result, err := SysUser.Get()
//tools.HasError(err, "抱歉未找到相关信息", -1)
//var SysRole models.SysRole
//var Post models.Post
//var Dept models.SysDepts
////获取角色列表
//roles, err := SysRole.GetList()
////获取职位列表
//posts, err := Post.GetList()
////获取部门列表
//Dept.DeptId = result.DeptId
//dept, err := Dept.Get()
//
//postIds := make([]int, 0)
//postIds = append(postIds, result.PostId)
//
//roleIds := make([]int, 0)
//roleIds = append(roleIds, result.RoleId)
//
//app.Custum(c, gin.H{
// "code": 200,
// "data": result,
// "postIds": postIds,
// "roleIds": roleIds,
// "roles": roles,
// "posts": posts,
// "dept": dept,
//})
}
func (e SysUser) GetInfo(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//数据权限检查
p := actions.GetPermissionFromContext(c)
var roles = make([]string, 1)
roles[0] = user.GetRoleName(c)
var permissions = make([]string, 1)
permissions[0] = "*:*:*"
var buttons = make([]string, 1)
buttons[0] = "*:*:*"
RoleMenu := system.RoleMenu{}
RoleMenu.RoleId = user.GetRoleId(c)
var mp = make(map[string]interface{})
mp["roles"] = roles
if user.GetRoleName(c) == "admin" || user.GetRoleName(c) == "系统管理员" {
mp["permissions"] = permissions
mp["buttons"] = buttons
} else {
list, _ := RoleMenu.GetPermis(db)
mp["permissions"] = list
mp["buttons"] = list
}
var sysUser system.SysUser
req := new(dto.SysUserById)
req.Id = user.GetUserId(c)
serviceSysUser := service.SysUser{}
serviceSysUser.Log = log
serviceSysUser.Orm = db
err = serviceSysUser.GetSysUser(req, p, &sysUser)
if err != nil {
e.Error(http.StatusUnauthorized, err, "登录失败")
return
}
mp["introduction"] = " am a super administrator"
mp["avatar"] = "https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif"
if sysUser.Avatar != "" {
mp["avatar"] = sysUser.Avatar
}
mp["userName"] = sysUser.NickName
mp["userId"] = sysUser.UserId
mp["deptId"] = sysUser.DeptId
mp["name"] = sysUser.NickName
e.OK(mp, "")
}
+175
View File
@@ -0,0 +1,175 @@
package apis
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
mycasbin "github.com/go-admin-team/go-admin-core/v2/casbin"
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
"github.com/go-admin-team/go-admin-core/v2/logger"
"github.com/go-admin-team/go-admin-core/v2/sdk"
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
"gorm.io/gorm"
"go-admin/app/admin/models"
)
// PUT /api/v1/sys-user is in settings.go's CasbinExclude so the
// personal-center screen (go-admin-ui's userInfo.vue) can edit the caller's
// own record without holding a policy grant on this route. AuthCheckRole
// skips Enforce entirely for an excluded route, so this file's job is to pin
// what the handler itself now has to hold shut: the target userId comes from
// the request body, and nothing upstream of the handler ever checked it
// against the caller.
// setupPrivescDB wires an in-memory database and a Casbin enforcer with an
// empty policy - the state of a fresh install for any role but admin - under
// a tenant unique to the calling test, so mycasbin's process-wide enforcer
// cache can't hand one test's database to another.
func setupPrivescDB(t *testing.T) (*gorm.DB, string) {
t.Helper()
// Fatalf, not Skipf: this database is in-memory sqlite with no external
// dependency, so failing to open or migrate it means the environment is
// actually broken. Skipping here would let these two anti-privesc
// regression tests silently stop running while CI stays green - a
// standing assertion that never fires is worse than no assertion.
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("sqlite unavailable: %v", err)
}
if err := db.AutoMigrate(&models.SysUser{}); err != nil {
t.Fatalf("automigrate: %v", err)
}
tenant := "sys-user-privesc-" + t.Name()
previousInterval := mycasbin.ReloadInterval
mycasbin.ReloadInterval = 0 // opt out of the background reload goroutine; the test never writes a policy
t.Cleanup(func() { mycasbin.ReloadInterval = previousInterval })
e := mycasbin.Setup(db, tenant)
previousEnforcer := sdk.Runtime.GetCasbinByTenant(tenant)
sdk.Runtime.SetCasbinByTenant(tenant, e)
t.Cleanup(func() { sdk.Runtime.SetCasbinByTenant(tenant, previousEnforcer) })
return db, tenant
}
// callUpdate drives SysUser.Update the way the router does for an
// authenticated, non-admin caller: JWT claims already decoded into the
// context (that is jwtauth's job, not this handler's) and a database - but
// without AuthCheckRole, since that middleware never runs Enforce for this
// route at all.
func callUpdate(t *testing.T, db *gorm.DB, tenant string, callerId int, body map[string]interface{}) *httptest.ResponseRecorder {
t.Helper()
gin.SetMode(gin.TestMode)
raw, err := json.Marshal(body)
if err != nil {
t.Fatalf("marshal request body: %v", err)
}
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/sys-user", bytes.NewReader(raw))
c.Request.Host = tenant
c.Request.Header.Set("Content-Type", "application/json")
c.Set("db", db)
c.Set(pkg.LoggerKey, logger.NewHelper(logger.DefaultLogger))
c.Set(jwt.JwtPayloadKey, jwt.MapClaims{
"identity": float64(callerId),
"rolekey": "ordinary-role", // holds no Casbin policy anywhere in this test
})
SysUser{}.Update(c)
return w
}
// TestUpdate_CannotEscalatePrivilegeThroughAnotherUsersRecord is the
// regression for H6. Before the fix, an ordinary authenticated user could PUT
// a body naming another user's id and change that user's roleId - the route
// being Casbin-excluded meant no permission check ever ran, and the data
// permission scope that would otherwise gate this is off by default.
func TestUpdate_CannotEscalatePrivilegeThroughAnotherUsersRecord(t *testing.T) {
db, tenant := setupPrivescDB(t)
victim := models.SysUser{Username: "bob", NickName: "Bob", RoleId: 2, DeptId: 1, Status: "1"}
if err := db.Create(&victim).Error; err != nil {
t.Fatal(err)
}
attacker := models.SysUser{Username: "alice", NickName: "Alice", RoleId: 2, DeptId: 1, Status: "1"}
if err := db.Create(&attacker).Error; err != nil {
t.Fatal(err)
}
const elevatedRoleId = 1 // a role the attacker does not hold and has no policy for
callUpdate(t, db, tenant, attacker.UserId, map[string]interface{}{
"userId": victim.UserId,
"username": victim.Username,
"nickName": "pwned",
"phone": "13800000000",
"email": "bob@example.com",
"roleId": elevatedRoleId,
"deptId": victim.DeptId,
"status": victim.Status,
})
var after models.SysUser
if err := db.First(&after, victim.UserId).Error; err != nil {
t.Fatal(err)
}
if after.RoleId == elevatedRoleId {
t.Fatalf("an attacker with no Casbin permission on this route escalated the victim's roleId to %d", after.RoleId)
}
if after.NickName == "pwned" {
t.Fatalf("an attacker with no Casbin permission on this route modified another user's record: %+v", after)
}
}
// TestUpdate_SelfEditCannotChangePrivilegedFields covers the case the
// CasbinExclude entry exists for: the personal-center screen has to keep
// working for the caller's own record. The fields that screen exposes
// (nickName/phone/email/sex) must still save, while roleId/deptId/status stay
// whatever the database already had even if the request carries something
// else - a compromised or hand-crafted client is the only way that request
// would ever differ from what the honest form sends.
func TestUpdate_SelfEditCannotChangePrivilegedFields(t *testing.T) {
db, tenant := setupPrivescDB(t)
self := models.SysUser{Username: "carol", NickName: "Carol", RoleId: 2, DeptId: 1, Status: "1"}
if err := db.Create(&self).Error; err != nil {
t.Fatal(err)
}
const elevatedRoleId = 1
callUpdate(t, db, tenant, self.UserId, map[string]interface{}{
"userId": self.UserId,
"username": self.Username,
"nickName": "Carol Updated",
"phone": "13900000000",
"email": "carol@example.com",
"roleId": elevatedRoleId, // tampered; must not take effect
"deptId": self.DeptId,
"status": self.Status,
})
var after models.SysUser
if err := db.First(&after, self.UserId).Error; err != nil {
t.Fatal(err)
}
if after.RoleId == elevatedRoleId {
t.Fatalf("a self-edit changed the caller's own roleId to %d", after.RoleId)
}
if after.NickName != "Carol Updated" {
t.Fatalf("the legitimate personal-center edit did not go through: %+v", after)
}
}
-28
View File
@@ -1,28 +0,0 @@
package system
import (
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/pkg/captcha"
"go-admin/common/apis"
)
type System struct {
apis.Api
}
func (e System) GenerateCaptchaHandler(c *gin.Context) {
e.Context = c
log := e.GetLogger()
id, b64s, err := captcha.DriverDigitFunc()
if err != nil {
log.Errorf("DriverDigitFunc error, %s", err.Error())
e.Error(500, err, "验证码获取失败")
return
}
e.Custom(gin.H{
"code": 200,
"data": b64s,
"id": id,
"msg": "success",
})
}
-269
View File
@@ -1,269 +0,0 @@
package dict
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
"go-admin/app/admin/models/system"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/apis"
common "go-admin/common/models"
)
type SysDictData struct {
apis.Api
}
// @Summary 字典数据列表
// @Description 获取JSON
// @Tags 字典数据
// @Param status query string false "status"
// @Param dictCode query string false "dictCode"
// @Param dictType query string false "dictType"
// @Param pageSize query int false "页条数"
// @Param pageIndex query int false "页码"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/dict/data [get]
// @Security Bearer
func (e SysDictData) GetSysDictDataList(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
req := &dto.SysDictDataSearch{}
//查询列表
err = req.Bind(c)
if err != nil {
log.Warnf("Bind error: %s", err.Error())
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
list := make([]system.SysDictData, 0)
var count int64
s := service.SysDictData{}
s.Log = log
s.Orm = db.Debug()
err = s.GetPage(req, &list, &count)
if err != nil {
log.Errorf("GetPage error, %s", err.Error())
e.Error(http.StatusInternalServerError, err, "查询失败")
return
}
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}
// @Summary 通过编码获取字典数据
// @Description 获取JSON
// @Tags 字典数据
// @Param dictCode path int true "字典编码"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/dict/data/{dictCode} [get]
// @Security Bearer
func (e SysDictData) GetSysDictData(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查看详情
req := &dto.SysDictDataById{}
err = req.Bind(c)
if err != nil {
log.Warnf("Bind error: %s", err.Error())
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
var object system.SysDictData
s := service.SysDictData{}
s.Log = log
s.Orm = db
err = s.Get(req, &object)
if err != nil {
log.Warnf("Get error: %s", err.Error())
e.Error(http.StatusInternalServerError, err, "查询失败")
return
}
e.OK(object, "查看成功")
}
// @Summary 添加字典数据
// @Description 获取JSON
// @Tags 字典数据
// @Accept application/json
// @Product application/json
// @Param data body dto.SysDictDataControl true "data"
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/dict/data [post]
// @Security Bearer
func (e SysDictData) InsertSysDictData(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//新增操作
req := &dto.SysDictDataControl{}
err = req.Bind(c)
if err != nil {
log.Warnf("Bind error: %s", err.Error())
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, _ := req.GenerateM()
// 设置创建人
object.SetCreateBy(user.GetUserId(c))
s := service.SysDictData{}
s.Orm = db
s.Log = log
err = s.Insert(object.(*system.SysDictData))
if err != nil {
log.Errorf("Insert error, %s", err)
e.Error(http.StatusInternalServerError, err, "创建失败")
return
}
e.OK(object.GetId(), "创建成功")
}
// @Summary 修改字典数据
// @Description 获取JSON
// @Tags 字典数据
// @Accept application/json
// @Product application/json
// @Param data body dto.SysDictDataControl true "body"
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/dict/data/{dictCode} [put]
// @Security Bearer
func (e SysDictData) UpdateSysDictData(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
req := &dto.SysDictDataControl{}
//更新操作
err = req.Bind(c)
if err != nil {
log.Warnf("request validate error, %s", err.Error())
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, _ := req.GenerateM()
object.SetUpdateBy(user.GetUserId(c))
s := service.SysDictData{}
s.Orm = db
s.Log = log
err = s.Update(object.(*system.SysDictData))
if err != nil {
log.Errorf("Update error, %s", err)
e.Error(http.StatusInternalServerError, err, "更新失败")
return
}
e.OK(object.GetId(), "更新成功")
}
// @Summary 删除字典数据
// @Description 删除数据
// @Tags 字典数据
// @Param dictCode path int true "dictCode"
// @Success 200 {string} string "{"code": 200, "message": "删除成功"}"
// @Success 200 {string} string "{"code": -1, "message": "删除失败"}"
// @Router /api/v1/dict/data/{dictCode} [delete]
func (e SysDictData) DeleteSysDictData(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//删除操作
req := new(dto.SysDictDataById)
err = req.Bind(c)
if err != nil {
log.Warnf("Bind error: %s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
var object common.ActiveRecord
object, err = req.GenerateM()
if err != nil {
log.Errorf("GenerateM error, %s", err.Error())
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
// 设置编辑人
object.SetUpdateBy(user.GetUserId(c))
s := service.SysDictData{}
s.Orm = db
s.Log = log
err = s.Remove(req, object.(*system.SysDictData))
if err != nil {
log.Errorf("Remove error, %s", err)
e.Error(http.StatusInternalServerError, err, "删除失败")
return
}
e.OK(object.GetId(), "删除成功")
}
func (e SysDictData) GetSysDictDataAll(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
req := &dto.SysDictDataSearch{}
//查询列表
err = req.Bind(c)
if err != nil {
log.Warnf("Bind error: %s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
list := make([]system.SysDictData, 0)
s := service.SysDictData{}
s.Log = log
s.Orm = db
err = s.GetAll(req, &list)
if err != nil {
log.Errorf("GetAll error, %s", err.Error())
e.Error(http.StatusInternalServerError, err, "查询失败")
return
}
e.OK(list, "查询成功")
}
-267
View File
@@ -1,267 +0,0 @@
package dict
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
"go-admin/app/admin/models/system"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/apis"
common "go-admin/common/models"
)
type SysDictType struct {
apis.Api
}
// @Summary 字典类型列表数据
// @Description 获取JSON
// @Tags 字典类型
// @Param dictName query string false "dictName"
// @Param dictId query string false "dictId"
// @Param dictType query string false "dictType"
// @Param pageSize query int false "页条数"
// @Param pageIndex query int false "页码"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/dict/type [get]
// @Security Bearer
func (e SysDictType) GetSysDictTypeList(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
req := &dto.SysDictTypeSearch{}
//查询列表
err = req.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
list := make([]system.SysDictType, 0)
var count int64
s := service.SysDictType{}
s.Log = log
s.Orm = db.Debug()
err = s.GetPage(req, &list, &count)
if err != nil {
e.Error(http.StatusInternalServerError, err, "查询失败")
return
}
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}
// @Summary 通过字典id获取字典类型
// @Description 获取JSON
// @Tags 字典类型
// @Param dictId path int true "字典类型编码"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/dict/type/{dictId} [get]
// @Security Bearer
func (e SysDictType) GetSysDictType(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查看详情
req := &dto.SysDictTypeById{}
err = req.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
var object system.SysDictType
s := service.SysDictType{}
s.Log = log
s.Orm = db
err = s.Get(req, &object)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.OK(object, "查看成功")
}
// @Summary 添加字典类型
// @Description 获取JSON
// @Tags 字典类型
// @Accept application/json
// @Product application/json
// @Param data body dto.SysDictTypeControl true "data"
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/dict/type [post]
// @Security Bearer
func (e SysDictType) InsertSysDictType(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//新增操作
req := &dto.SysDictTypeControl{}
err = req.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, _ := req.GenerateM()
// 设置创建人
object.SetCreateBy(user.GetUserId(c))
s := service.SysDictType{}
s.Orm = db
s.Log = log
err = s.Insert(object.(*system.SysDictType))
if err != nil {
log.Error(err)
e.Error(http.StatusInternalServerError, err, "创建失败")
return
}
e.OK(object.GetId(), "创建成功")
}
// @Summary 修改字典类型
// @Description 获取JSON
// @Tags 字典类型
// @Accept application/json
// @Product application/json
// @Param data body dto.SysDictTypeControl true "body"
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/dict/type/{dictId} [put]
// @Security Bearer
func (e SysDictType) UpdateSysDictType(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
req := &dto.SysDictTypeControl{}
//更新操作
err = req.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, _ := req.GenerateM()
object.SetUpdateBy(user.GetUserId(c))
s := service.SysDictType{}
s.Orm = db
s.Log = log
err = s.Update(object.(*system.SysDictType))
if err != nil {
log.Error(err)
return
}
e.OK(object.GetId(), "更新成功")
}
// @Summary 删除字典类型
// @Description 删除数据
// @Tags 字典类型
// @Param dictId path int true "dictId"
// @Success 200 {string} string "{"code": 200, "message": "删除成功"}"
// @Success 200 {string} string "{"code": -1, "message": "删除失败"}"
// @Router /api/v1/dict/type/{dictId} [delete]
func (e SysDictType) DeleteSysDictType(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//删除操作
req := new(dto.SysDictTypeById)
err = req.Bind(c)
if err != nil {
log.Errorf("Bind error: %s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
var object common.ActiveRecord
object, err = req.GenerateM()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
// 设置编辑人
object.SetUpdateBy(user.GetUserId(c))
s := service.SysDictType{}
s.Orm = db
s.Log = log
err = s.Remove(req, object.(*system.SysDictType))
if err != nil {
log.Error(err)
return
}
e.OK(object.GetId(), "删除成功")
}
// @Summary 字典类型全部数据
// @Description 获取JSON
// @Tags 字典类型
// @Param dictName query string false "dictName"
// @Param dictId query string false "dictId"
// @Param dictType query string false "dictType"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/dict/type-option-select [get]
// @Security Bearer
func (e SysDictType) GetSysDictTypeAll(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
req := &dto.SysDictTypeSearch{}
//查询列表
err = req.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
list := make([]system.SysDictType, 0)
s := service.SysDictType{}
s.Log = log
s.Orm = db
err = s.GetAll(req, &list)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.OK(list, "查询成功")
}
-98
View File
@@ -1,98 +0,0 @@
package system
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/apis"
)
type SysSetting struct {
apis.Api
}
// @Summary 查询系统信息
// @Description 获取JSON
// @Tags 系统信息
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/setting [get]
func (e SysSetting) GetSetting(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
sysSettingService := service.SysSetting{}
sysSettingService.Log = log
sysSettingService.Orm = db
var model = models.SysSetting{}
err = sysSettingService.GetSysSetting(&model)
if err != nil {
e.Error(http.StatusInternalServerError, err, "查询失败")
return
}
if model.Logo != "" {
if !strings.HasPrefix(model.Logo, "http") {
model.Logo = fmt.Sprintf("http://%s/%s", c.Request.Host, model.Logo)
}
}
e.OK(model, "查询成功")
}
// @Summary 更新或提交系统信息
// @Description 获取JSON
// @Tags 系统信息
// @Param data body dto.SysSettingControl true "body"
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/system/setting [post]
func (e SysSetting) CreateOrUpdateSetting(c *gin.Context) {
control := new(dto.SysSettingControl)
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//更新操作
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, err := control.Generate()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
sysSettingService := service.SysSetting{}
sysSettingService.Log = log
sysSettingService.Orm = db
err = sysSettingService.UpdateSysSetting(object)
if err != nil {
e.Error(http.StatusInternalServerError, err, "更新失败")
return
}
if object.Logo != "" {
if !strings.HasPrefix(object.Logo, "http") {
object.Logo = fmt.Sprintf("http://%s/%s", c.Request.Host, object.Logo)
}
}
e.OK(object, "提交成功")
}
@@ -1,277 +0,0 @@
package sys_config
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
"go-admin/app/admin/models/system"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/apis"
)
type SysConfig struct {
apis.Api
}
func (e SysConfig) GetSysConfigList(c *gin.Context) {
e.Context = c
log := e.GetLogger()
d := new(dto.SysConfigSearch)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查询列表
err = d.Bind(c)
if err != nil {
log.Errorf("参数验证失败, error:%s", err)
e.Error(500, err, "参数验证失败")
return
}
list := make([]system.SysConfig, 0)
var count int64
s := service.SysConfig{}
s.Log = log
s.Orm = db
err = s.GetSysConfigPage(d, &list, &count)
if err != nil {
log.Errorf("GetSysConfigPage 查询失败, error:%s", err)
e.Error(500, err, "查询失败")
return
}
e.PageOK(list, int(count), d.GetPageIndex(), d.GetPageSize(), "查询成功")
}
// GetSysConfigBySysApp 获取系统配置信息,主要注意这里不在验证数据权限
func (e SysConfig) GetSysConfigBySysApp(c *gin.Context) {
e.Context = c
log := e.GetLogger()
d := new(dto.SysConfigSearch)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
err = d.Bind(c)
if err != nil {
log.Errorf("参数验证失败, error:%s", err)
e.Error(500, err, "参数验证失败")
return
}
// 控制只读前台的数据
d.IsFrontend = 1
list := make([]system.SysConfig, 0)
s := service.SysConfig{}
s.Log = log
s.Orm = db
err = s.GetSysConfigByKey(d, &list)
if err != nil {
log.Errorf("GetSysConfigPage 查询失败, error:%s", err)
e.Error(500, err, "查询失败")
return
}
mp := make(map[string]string)
for i := 0; i < len(list); i++ {
key := list[i].ConfigKey
if key != "" {
mp[key] = list[i].ConfigValue
}
}
e.OK(mp, "查询成功")
}
func (e SysConfig) GetSysConfig(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysConfigById)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查看详情
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
log.Errorf("Orm获取失败, error:%s", err)
e.Error(500, err, "Orm获取失败")
return
}
var object system.SysConfig
serviceSysLoginLog := service.SysConfig{}
serviceSysLoginLog.Log = log
serviceSysLoginLog.Orm = db
err = serviceSysLoginLog.GetSysConfig(control, &object)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
log.Errorf("Orm获取失败, error:%s", err)
e.Error(500, err, "Orm获取失败")
return
}
e.OK(object, "查看成功")
}
func (e SysConfig) InsertSysConfig(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysConfigControl)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//新增操作
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
log.Errorf("Orm获取失败, error:%s", err)
e.Error(500, err, "Orm获取失败")
return
}
object, err := control.Generate()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
log.Errorf("Orm获取失败, error:%s", err)
e.Error(500, err, "Orm获取失败")
return
}
// 设置创建人
object.SetCreateBy(user.GetUserId(c))
serviceSysLoginLog := service.SysConfig{}
serviceSysLoginLog.Orm = db
serviceSysLoginLog.Log = log
err = serviceSysLoginLog.InsertSysConfig(object)
if err != nil {
log.Error(err)
e.Error(http.StatusInternalServerError, err, "创建失败")
log.Errorf("Orm获取失败, error:%s", err)
e.Error(500, err, "Orm获取失败")
return
}
e.OK(object.GetId(), "创建成功")
}
func (e SysConfig) UpdateSysConfig(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysConfigControl)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//更新操作
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
log.Errorf("Orm获取失败, error:%s", err)
e.Error(500, err, "Orm获取失败")
return
}
object, err := control.Generate()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
log.Errorf("Orm获取失败, error:%s", err)
e.Error(500, err, "Orm获取失败")
return
}
object.SetUpdateBy(user.GetUserId(c))
serviceSysLoginLog := service.SysConfig{}
serviceSysLoginLog.Orm = db
serviceSysLoginLog.Log = log
err = serviceSysLoginLog.UpdateSysConfig(object)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "更新失败")
log.Errorf("Orm获取失败, error:%s", err)
e.Error(500, err, "Orm获取失败")
return
}
e.OK(object.GetId(), "更新成功")
}
func (e SysConfig) DeleteSysConfig(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysConfigById)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//删除操作
err = control.Bind(c)
if err != nil {
log.Errorf("Bind error: %s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
log.Errorf("Orm获取失败, error:%s", err)
e.Error(500, err, "Orm获取失败")
return
}
object, err := control.GenerateM()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
log.Errorf("Orm获取失败, error:%s", err)
e.Error(500, err, "Orm获取失败")
return
}
// 设置编辑人
object.SetUpdateBy(user.GetUserId(c))
serviceSysLoginLog := service.SysConfig{}
serviceSysLoginLog.Orm = db
serviceSysLoginLog.Log = log
err = serviceSysLoginLog.RemoveSysConfig(control, object)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "删除失败")
log.Errorf("Orm获取失败, error:%s", err)
e.Error(500, err, "Orm获取失败")
return
}
e.OK(object.GetId(), "删除成功")
}
// GetSysConfigByKEYForService 根据Key获取SysConfig的Service
func (e SysConfig) GetSysConfigByKEYForService(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
var v dto.SysConfigControl
err = v.Bind(c)
if err != nil {
log.Errorf("参数验证错误, error:%s", err)
e.Error(422, err, "参数验证失败")
return
}
s := service.SysConfig{}
s.Log = log
s.Orm = db
err = s.GetSysConfigByKEY(&v)
if err != nil {
log.Errorf("通过Key获取配置失败, error:%s", err)
e.Error(500, err, "")
return
}
e.OK(v, s.Msg)
}
-294
View File
@@ -1,294 +0,0 @@
package sys_dept
import (
"github.com/go-admin-team/go-admin-core/sdk/pkg"
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
"go-admin/app/admin/models/system"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/apis"
)
type SysDept struct {
apis.Api
}
// @Summary 分页部门列表数据
// @Description 分页列表
// @Tags 部门
// @Param name query string false "name"
// @Param id query string false "id"
// @Param position query string false "position"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/dept [get]
// @Security Bearer
func (e SysDept) GetSysDeptList(c *gin.Context) {
e.Context = c
log := e.GetLogger()
d := new(dto.SysDeptSearch)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查询列表
err = d.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
list := make([]system.SysDept, 0)
serviceStudent := service.SysDept{}
serviceStudent.Log = log
serviceStudent.Orm = db
list, err = serviceStudent.SetDeptPage(d)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.OK(list, "查询成功")
}
// @Summary 部门列表数据
// @Description 获取JSON
// @Tags 部门
// @Param deptId path string false "deptId"
// @Param position query string false "position"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/dept/{deptId} [get]
// @Security Bearer
func (e SysDept) GetSysDept(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysDeptById)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查看详情
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
var object system.SysDept
serviceSysOperlog := service.SysDept{}
serviceSysOperlog.Log = log
serviceSysOperlog.Orm = db
err = serviceSysOperlog.GetSysDept(control, &object)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.OK(object, "查看成功")
}
// @Summary 添加部门
// @Description 获取JSON
// @Tags 部门
// @Accept application/json
// @Product application/json
// @Param data body dto.SysDeptControl true "data"
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/dept [post]
// @Security Bearer
func (e SysDept) InsertSysDept(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysDeptControl)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//新增操作
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, err := control.Generate()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
// 设置创建人
object.SetCreateBy(user.GetUserId(c))
serviceSysDept := service.SysDept{}
serviceSysDept.Orm = db
serviceSysDept.Log = log
err = serviceSysDept.InsertSysDept(object)
if err != nil {
log.Error(err)
e.Error(http.StatusInternalServerError, err, "创建失败")
return
}
e.OK(object.GetId(), "创建成功")
}
// @Summary 修改部门
// @Description 获取JSON
// @Tags 部门
// @Accept application/json
// @Product application/json
// @Param id path int true "id"
// @Param data body dto.SysDeptControl true "body"
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/dept/{deptId} [put]
// @Security Bearer
func (e SysDept) UpdateSysDept(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysDeptControl)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//更新操作
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, err := control.Generate()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
object.SetUpdateBy(user.GetUserId(c))
serviceSysDept := service.SysDept{}
serviceSysDept.Orm = db
serviceSysDept.Log = log
err = serviceSysDept.UpdateSysDept(object)
if err != nil {
log.Error(err)
return
}
e.OK(object.GetId(), "更新成功")
}
// @Summary 删除部门
// @Description 删除数据
// @Tags 部门
// @Param data body dto.SysDeptById true "body"
// @Success 200 {string} string "{"code": 200, "message": "删除成功"}"
// @Success 200 {string} string "{"code": -1, "message": "删除失败"}"
// @Router /api/v1/dept [delete]
func (e SysDept) DeleteSysDept(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysDeptById)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//删除操作
err = control.Bind(c)
if err != nil {
log.Errorf("Bind error: %s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
serviceSysDept := service.SysDept{}
serviceSysDept.Orm = db
serviceSysDept.Log = log
err = serviceSysDept.RemoveSysDept(control)
if err != nil {
log.Errorf("RemoveSysDept error, %s", err.Error())
e.Error(http.StatusInternalServerError, err, "删除失败")
return
}
e.OK(control.GetId(), "删除成功")
}
// GetDeptTree 用户管理 左侧部门树
func (e SysDept) GetDeptTree(c *gin.Context) {
e.Context = c
log := e.GetLogger()
d := new(dto.SysDeptSearch)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查询列表
err = d.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
list := make([]dto.DeptLabel, 0)
serviceStudent := service.SysDept{}
serviceStudent.Log = log
serviceStudent.Orm = db
list, err = serviceStudent.SetDeptTree(d)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
//var Dept models.SysDepts
//Dept.DeptName = c.Request.FormValue("deptName")
//Dept.Status = c.Request.FormValue("status")
//Dept.DeptId, _ = tools.StringToInt(c.Request.FormValue("deptId"))
//result, err := Dept.SetDept(false)
//tools.HasError(err, "抱歉未找到相关信息", -1)
e.OK(list, "")
}
func (e SysDept) GetDeptTreeRoleSelect(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
s := service.SysDept{}
s.Orm = db
s.Log = log
id, err := pkg.StringToInt(c.Param("roleId"))
result, err := s.SetDeptLabel()
if err != nil {
log.Errorf("SetDeptLabel error, %s", err.Error())
e.Error(http.StatusInternalServerError, err, "")
}
menuIds := make([]int, 0)
if id != 0 {
menuIds, err = s.GetRoleDeptId(id)
if err != nil {
log.Errorf("抱歉未找到相关信息, %s", err.Error())
e.Error(http.StatusInternalServerError, err, "")
}
}
e.OK(gin.H{
"depts": result,
"checkedKeys": menuIds,
}, "")
}
@@ -1,188 +0,0 @@
package sys_login_log
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
"go-admin/app/admin/models/system"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/apis"
)
type SysLoginLog struct {
apis.Api
}
func (e SysLoginLog) GetSysLoginLogList(c *gin.Context) {
e.Context = c
log := e.GetLogger()
d := new(dto.SysLoginLogSearch)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查询列表
err = d.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
list := make([]system.SysLoginLog, 0)
var count int64
serviceStudent := service.SysLoginLog{}
serviceStudent.Log = log
serviceStudent.Orm = db
err = serviceStudent.GetSysLoginLogPage(d, &list, &count)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.PageOK(list, int(count), d.GetPageIndex(), d.GetPageSize(), "查询成功")
}
func (e SysLoginLog) GetSysLoginLog(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysLoginLogById)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查看详情
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
var object system.SysLoginLog
serviceSysLoginLog := service.SysLoginLog{}
serviceSysLoginLog.Log = log
serviceSysLoginLog.Orm = db
err = serviceSysLoginLog.GetSysLoginLog(control, &object)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.OK(object, "查看成功")
}
func (e SysLoginLog) InsertSysLoginLog(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysLoginLogControl)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//新增操作
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, err := control.Generate()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
// 设置创建人
object.SetCreateBy(user.GetUserId(c))
serviceSysLoginLog := service.SysLoginLog{}
serviceSysLoginLog.Orm = db
serviceSysLoginLog.Log = log
err = serviceSysLoginLog.InsertSysLoginLog(object)
if err != nil {
log.Errorf("InsertSysLoginLog error, %s", err)
e.Error(http.StatusInternalServerError, err, "创建失败")
return
}
e.OK(object.GetId(), "创建成功")
}
func (e SysLoginLog) UpdateSysLoginLog(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysLoginLogControl)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//更新操作
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, err := control.Generate()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
object.SetUpdateBy(user.GetUserId(c))
serviceSysLoginLog := service.SysLoginLog{}
serviceSysLoginLog.Orm = db
serviceSysLoginLog.Log = log
err = serviceSysLoginLog.UpdateSysLoginLog(object)
if err != nil {
log.Errorf("UpdateSysLoginLog error, %s", err)
e.Error(http.StatusInternalServerError, err, "更新失败")
return
}
e.OK(object.GetId(), "更新成功")
}
func (e SysLoginLog) DeleteSysLoginLog(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysLoginLogById)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//删除操作
err = control.Bind(c)
if err != nil {
log.Errorf("Bind error: %s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, err := control.GenerateM()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
// 设置编辑人
object.SetUpdateBy(user.GetUserId(c))
serviceSysLoginLog := service.SysLoginLog{}
serviceSysLoginLog.Orm = db
serviceSysLoginLog.Log = log
err = serviceSysLoginLog.RemoveSysLoginLog(control, object)
if err != nil {
log.Errorf("RemoveSysLoginLog error, %s", err)
e.Error(http.StatusInternalServerError, err, "删除失败")
return
}
e.OK(object.GetId(), "删除成功")
}
-369
View File
@@ -1,369 +0,0 @@
package sys_menu
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
"go-admin/app/admin/models/system"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/apis"
)
type SysMenu struct {
apis.Api
}
// @Summary Menu列表数据
// @Description 获取JSON
// @Tags 菜单
// @Param menuName query string false "menuName"
// @Success 200 {string} string "{"code": 200, "data": [...]}"
// @Success 200 {string} string "{"code": -1, "message": "抱歉未找到相关信息"}"
// @Router /api/v1/menulist [get]
// @Security Bearer
func (e SysMenu) GetSysMenuList(c *gin.Context) {
e.Context = c
log := e.GetLogger()
d := new(dto.SysMenuSearch)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查询列表
err = d.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
var list *[]system.SysMenu
serviceSysMenu := service.SysMenu{}
serviceSysMenu.Log = log
serviceSysMenu.Orm = db
list, err = serviceSysMenu.GetSysMenuPage(d)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.OK(list, "查询成功")
}
// @Summary Menu详情数据
// @Description 获取JSON
// @Tags 菜单
// @Param menuName query string false "menuName"
// @Success 200 {string} string "{"code": 200, "data": [...]}"
// @Success 200 {string} string "{"code": -1, "message": "抱歉未找到相关信息"}"
// @Router /api/v1/menu/{id} [get]
// @Security Bearer
func (e SysMenu) GetSysMenu(c *gin.Context) {
control := new(dto.SysMenuById)
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查看详情
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
var object system.SysMenu
serviceSysMenu := service.SysMenu{}
serviceSysMenu.Log = log
serviceSysMenu.Orm = db
err = serviceSysMenu.GetSysMenu(control, &object)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.OK(object, "查看成功")
}
// @Summary 创建菜单
// @Description 获取JSON
// @Tags 菜单
// @Accept application/x-www-form-urlencoded
// @Product application/x-www-form-urlencoded
// @Param menuName formData string true "menuName"
// @Param Path formData string false "Path"
// @Param Action formData string true "Action"
// @Param Permission formData string true "Permission"
// @Param ParentId formData string true "ParentId"
// @Param IsDel formData string true "IsDel"
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/menu [post]
// @Security Bearer
func (e SysMenu) InsertSysMenu(c *gin.Context) {
control := new(dto.SysMenuControl)
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//新增操作
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, err := control.Generate()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
// 设置创建人
object.SetCreateBy(user.GetUserId(c))
serviceSysMenu := service.SysMenu{}
serviceSysMenu.Orm = db
serviceSysMenu.Log = log
err = serviceSysMenu.InsertSysMenu(object)
if err != nil {
log.Error(err)
e.Error(http.StatusInternalServerError, err, "创建失败")
return
}
e.OK(object.GetId(), "创建成功")
}
// @Summary 修改菜单
// @Description 获取JSON
// @Tags 菜单
// @Accept application/x-www-form-urlencoded
// @Product application/x-www-form-urlencoded
// @Param id path int true "id"
// @Param data body dto.SysMenuControl true "body"
// @Success 200 {string} string "{"code": 200, "message": "修改成功"}"
// @Success 200 {string} string "{"code": -1, "message": "修改失败"}"
// @Router /api/v1/menu/{id} [put]
// @Security Bearer
func (e SysMenu) UpdateSysMenu(c *gin.Context) {
control := new(dto.SysMenuControl)
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//更新操作
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, err := control.Generate()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
object.SetUpdateBy(user.GetUserId(c))
serviceSysMenu := service.SysMenu{}
serviceSysMenu.Orm = db
serviceSysMenu.Log = log
err = serviceSysMenu.UpdateSysMenu(object)
if err != nil {
log.Error(err)
return
}
e.OK(object.GetId(), "更新成功")
}
// @Summary 删除菜单
// @Description 删除数据
// @Tags 菜单
// @Param data body dto.SysMenuById true "body"
// @Success 200 {string} string "{"code": 200, "message": "删除成功"}"
// @Success 200 {string} string "{"code": -1, "message": "删除失败"}"
// @Router /api/v1/menu/ [delete]
func (e SysMenu) DeleteSysMenu(c *gin.Context) {
control := new(dto.SysMenuById)
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//删除操作
err = control.Bind(c)
if err != nil {
log.Errorf("Bind error: %s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
serviceSysMenu := service.SysMenu{}
serviceSysMenu.Orm = db
serviceSysMenu.Log = log
err = serviceSysMenu.RemoveSysMenu(control)
if err != nil {
log.Errorf("RemoveSysMenu error, %s", err)
e.Error(http.StatusInternalServerError, err, "删除失败")
return
}
e.OK(control.GetId(), "删除成功")
}
// @Summary 根据角色名称获取菜单列表数据(左菜单使用)
// @Description 获取JSON
// @Tags 菜单
// @Param id path int true "id"
// @Success 200 {string} string "{"code": 200, "data": [...]}"
// @Success 200 {string} string "{"code": -1, "message": "抱歉未找到相关信息"}"
// @Router /api/v1/menurole [get]
// @Security Bearer
func (e SysMenu) GetMenuRole(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
serviceSysMenu := service.SysMenu{}
serviceSysMenu.Log = log
serviceSysMenu.Orm = db
result, err := serviceSysMenu.SetMenuRole(user.GetRoleName(c))
if err != nil {
e.Error(http.StatusInternalServerError, err, "查询失败")
return
}
e.OK(result, "")
}
// @Summary 获取角色对应的菜单id数组
// @Description 获取JSON
// @Tags 菜单
// @Param id path int true "id"
// @Success 200 {string} string "{"code": 200, "data": [...]}"
// @Success 200 {string} string "{"code": -1, "message": "抱歉未找到相关信息"}"
// @Router /api/v1/menuids/{id} [get]
// @Security Bearer
func (e SysMenu) GetMenuIDS(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
var data system.RoleMenu
data.RoleName = c.GetString("role")
data.UpdateBy = user.GetUserId(c)
result, err := data.GetIDS(db)
if err != nil {
log.Errorf("GetIDS error, %s", err.Error())
e.Error(http.StatusInternalServerError, err, "获取失败")
return
}
e.OK(result, "")
}
//// GetMenuTreeRoleselect 角色修改中的菜单列表
//func (e SysMenu) GetMenuTreeRoleselect(c *gin.Context) {
// var Menu models.Menu
// var SysRole models.SysRole
//
// id, err := tools.StringToInt(c.Param("roleId"))
// SysRole.RoleId = id
// //var r *models.SysRole
// r, err := SysRole.Get()
//
// var result *[]models.MenuLable
// menuIds := make([]int, 0)
// if r.RoleKey != "admin" {
// result, err = Menu.SetMenuLabel()
// tools.HasError(err, "抱歉未找到相关信息", -1)
// if id != 0 {
// menuIds, err = SysRole.GetRoleMeunId()
// tools.HasError(err, "抱歉未找到相关信息", -1)
// }
// }
// app.Custum(c, gin.H{
// "code": 200,
// "menus": result,
// "checkedKeys": menuIds,
// })
//}
// @Summary 获取菜单树
// @Description 获取JSON
// @Tags 菜单
// @Accept application/x-www-form-urlencoded
// @Product application/x-www-form-urlencoded
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/menuTreeselect [get]
// @Security Bearer
func (e SysMenu) GetMenuTreeSelect(c *gin.Context) {
e.Context = c
log := e.GetLogger()
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
d := new(dto.SelectRole)
err = c.BindUri(d)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
serviceSysMenu := service.SysMenu{}
serviceSysMenu.Log = log
serviceSysMenu.Orm = db
result, err := serviceSysMenu.SetSysMenuLabel()
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
s := service.SysRole{}
s.Log = log
s.Orm = db
menuIds, err := s.GetRoleMenuId(db, d.RoleId)
if err != nil {
log.Errorf("GetIDS error, %s", err.Error())
e.Error(http.StatusInternalServerError, err, "")
return
}
e.OK(gin.H{
"menus": result,
"checkedKeys": menuIds,
}, "获取成功")
}
@@ -1,178 +0,0 @@
package sys_opera_log
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
"go-admin/app/admin/models/system"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/apis"
)
type SysOperaLog struct {
apis.Api
}
func (e SysOperaLog) GetSysOperaLogList(c *gin.Context) {
e.Context = c
log := e.GetLogger()
d := new(dto.SysOperaLogSearch)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查询列表
err = d.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
list := make([]system.SysOperaLog, 0)
var count int64
serviceStudent := service.SysOperaLog{}
serviceStudent.Log = log
serviceStudent.Orm = db
err = serviceStudent.GetSysOperaLogPage(d, &list, &count)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.PageOK(list, int(count), d.GetPageIndex(), d.GetPageSize(), "查询成功")
}
func (e SysOperaLog) GetSysOperaLog(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysOperaLogById)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查看详情
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
var object system.SysOperaLog
serviceSysOperlog := service.SysOperaLog{}
serviceSysOperlog.Log = log
serviceSysOperlog.Orm = db
err = serviceSysOperlog.GetSysOperaLog(control, &object)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.OK(object, "查看成功")
}
func (e SysOperaLog) InsertSysOperaLog(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysOperaLogControl)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//新增操作
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, err := control.Generate()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
// 设置创建人
object.SetCreateBy(user.GetUserId(c))
serviceSysOperaLog := service.SysOperaLog{}
serviceSysOperaLog.Orm = db
serviceSysOperaLog.Log = log
err = serviceSysOperaLog.InsertSysOperaLog(object)
if err != nil {
log.Error(err)
e.Error(http.StatusInternalServerError, err, "创建失败")
return
}
e.OK(object.GetId(), "创建成功")
}
func (e SysOperaLog) UpdateSysOperaLog(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysOperaLogControl)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//更新操作
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, err := control.Generate()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
object.SetUpdateBy(user.GetUserId(c))
serviceSysOperaLog := service.SysOperaLog{}
serviceSysOperaLog.Orm = db
serviceSysOperaLog.Log = log
err = serviceSysOperaLog.UpdateSysOperaLog(object)
if err != nil {
log.Error(err)
return
}
e.OK(object.GetId(), "更新成功")
}
func (e SysOperaLog) DeleteSysOperaLog(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysOperaLogById)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//删除操作
err = control.Bind(c)
if err != nil {
log.Errorf("Bind error: %s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
serviceSysOperaLog := service.SysOperaLog{}
serviceSysOperaLog.Orm = db
serviceSysOperaLog.Log = log
err = serviceSysOperaLog.RemoveSysOperaLog(control)
if err != nil {
log.Error(err)
return
}
e.OK(control.GetId(), "删除成功")
}
-223
View File
@@ -1,223 +0,0 @@
package sys_post
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
"go-admin/app/admin/models/system"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/apis"
)
type SysPost struct {
apis.Api
}
// @Summary 岗位列表数据
// @Description 获取JSON
// @Tags 岗位
// @Param postName query string false "postName"
// @Param postCode query string false "postCode"
// @Param postId query string false "postId"
// @Param status query string false "status"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/post [get]
// @Security Bearer
func (e SysPost) GetSysPostList(c *gin.Context) {
e.Context = c
log := e.GetLogger()
d := new(dto.SysPostSearch)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查询列表
err = d.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
list := make([]system.SysPost, 0)
var count int64
serviceStudent := service.SysPost{}
serviceStudent.Log = log
serviceStudent.Orm = db
err = serviceStudent.GetSysPostPage(d, &list, &count)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.PageOK(list, int(count), d.GetPageIndex(), d.GetPageSize(), "查询成功")
}
// @Summary 获取岗位信息
// @Description 获取JSON
// @Tags 岗位
// @Param postId path int true "postId"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/post/{postId} [get]
// @Security Bearer
func (e SysPost) GetSysPost(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysPostById)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查看详情
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
var object system.SysPost
serviceSysOperlog := service.SysPost{}
serviceSysOperlog.Log = log
serviceSysOperlog.Orm = db
err = serviceSysOperlog.GetSysPost(control, &object)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.OK(object, "查看成功")
}
// @Summary 添加岗位
// @Description 获取JSON
// @Tags 岗位
// @Accept application/json
// @Product application/json
// @Param data body dto.SysPostControl true "data"
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/post [post]
// @Security Bearer
func (e SysPost) InsertSysPost(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysPostControl)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//新增操作
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, err := control.Generate()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
// 设置创建人
object.SetCreateBy(user.GetUserId(c))
serviceSysPost := service.SysPost{}
serviceSysPost.Orm = db
serviceSysPost.Log = log
err = serviceSysPost.InsertSysPost(object)
if err != nil {
log.Error(err)
e.Error(http.StatusInternalServerError, err, "创建失败")
return
}
e.OK(object.GetId(), "创建成功")
}
// @Summary 修改岗位
// @Description 获取JSON
// @Tags 岗位
// @Accept application/json
// @Product application/json
// @Param data body dto.SysPostControl true "body"
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/post/ [put]
// @Security Bearer
func (e SysPost) UpdateSysPost(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysPostControl)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//更新操作
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, err := control.Generate()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
object.SetUpdateBy(user.GetUserId(c))
serviceSysPost := service.SysPost{}
serviceSysPost.Orm = db
serviceSysPost.Log = log
err = serviceSysPost.UpdateSysPost(object)
if err != nil {
log.Error(err)
return
}
e.OK(object.GetId(), "更新成功")
}
// @Summary 删除岗位
// @Description 删除数据
// @Tags 岗位
// @Param id path int true "id"
// @Success 200 {string} string "{"code": 200, "message": "删除成功"}"
// @Success 500 {string} string "{"code": 500, "message": "删除失败"}"
// @Router /api/v1/post/{postId} [delete]
func (e SysPost) DeleteSysPost(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysPostById)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//删除操作
err = control.Bind(c)
if err != nil {
log.Errorf("Bind error: %s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
serviceSysPost := service.SysPost{}
serviceSysPost.Orm = db
serviceSysPost.Log = log
err = serviceSysPost.RemoveSysPost(control)
if err != nil {
log.Error(err)
return
}
e.OK(control.GetId(), "删除成功")
}
-279
View File
@@ -1,279 +0,0 @@
package sys_role
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
"go-admin/app/admin/models/system"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/apis"
"go-admin/common/global"
)
type SysRole struct {
apis.Api
}
// @Summary 角色列表数据
// @Description Get JSON
// @Tags 角色/Role
// @Param roleName query string false "roleName"
// @Param status query string false "status"
// @Param roleKey query string false "roleKey"
// @Param pageSize query int false "页条数"
// @Param pageIndex query int false "页码"
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
// @Router /api/v1/role [get]
// @Security Bearer
func (e SysRole) GetSysRoleList(c *gin.Context) {
e.Context = c
log := e.GetLogger()
d := new(dto.SysRoleSearch)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查询列表
err = d.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
list := make([]system.SysRole, 0)
var count int64
s := service.SysRole{}
s.Log = log
s.Orm = db
err = s.GetSysRolePage(d, &list, &count)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.PageOK(list, int(count), d.GetPageIndex(), d.GetPageSize(), "查询成功")
}
// @Summary 获取Role数据
// @Description 获取JSON
// @Tags 角色/Role
// @Param roleId path string false "roleId"
// @Success 200 {string} string "{"code": 200, "data": [...]}"
// @Success 200 {string} string "{"code": -1, "message": "抱歉未找到相关信息"}"
// @Router /api/v1/role/{id} [get]
// @Security Bearer
func (e SysRole) GetSysRole(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysRoleById)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//查看详情
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
var object system.SysRole
s := service.SysRole{}
s.Log = log
s.Orm = db
err = s.GetSysRole(control, &object)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "查询失败")
return
}
e.OK(object, "查看成功")
}
// @Summary 创建角色
// @Description 获取JSON
// @Tags 角色/Role
// @Accept application/json
// @Product application/json
// @Param data body dto.SysRoleControl true "data"
// @Success 200 {string} string "{"code": 200, "message": "添加成功"}"
// @Success 200 {string} string "{"code": -1, "message": "添加失败"}"
// @Router /api/v1/role [post]
// @Security Bearer
func (e SysRole) InsertSysRole(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysRoleControl)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//新增操作
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, err := control.Generate()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
// 设置创建人
object.CreateBy = user.GetUserId(c)
if object.Status == "" {
object.Status = "2"
}
s := service.SysRole{}
s.Orm = db
s.Log = log
err = s.InsertSysRole(object)
if err != nil {
log.Error(err)
e.Error(http.StatusInternalServerError, err, "创建失败")
return
}
_, err = global.LoadPolicy(c)
if err != nil {
e.Error(http.StatusInternalServerError, err, "")
return
}
e.OK(object.GetId(), "创建成功")
}
// @Summary 修改用户角色
// @Description 获取JSON
// @Tags 角色/Role
// @Accept application/json
// @Product application/json
// @Param data body dto.SysRoleControl true "body"
// @Success 200 {string} string "{"code": 200, "message": "修改成功"}"
// @Success 200 {string} string "{"code": -1, "message": "修改失败"}"
// @Router /api/v1/role/{id} [put]
// @Security Bearer
func (e SysRole) UpdateSysRole(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysRoleControl)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//更新操作
err = control.Bind(c)
if err != nil {
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
object, err := control.Generate()
if err != nil {
e.Error(http.StatusInternalServerError, err, "模型生成失败")
return
}
object.UpdateBy = user.GetUserId(c)
s := service.SysRole{}
s.Orm = db
s.Log = log
err = s.UpdateSysRole(object)
if err != nil {
log.Error(err)
return
}
_, err = global.LoadPolicy(c)
if err != nil {
e.Error(http.StatusInternalServerError, err, "")
return
}
e.OK(object.GetId(), "更新成功")
}
// @Summary 删除用户角色
// @Description 删除数据
// @Tags 角色/Role
// @Param data body dto.SysRoleById true "body"
// @Success 200 {string} string "{"code": 200, "message": "删除成功"}"
// @Success 200 {string} string "{"code": -1, "message": "删除失败"}"
// @Router /api/v1/role [delete]
// @Security Bearer
func (e SysRole) DeleteSysRole(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.SysRoleById)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//删除操作
err = control.Bind(c)
if err != nil {
log.Errorf("Bind error: %s", err)
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
s := service.SysRole{}
s.Orm = db
s.Log = log
err = s.RemoveSysRole(control)
if err != nil {
log.Error(err)
e.Error(http.StatusInternalServerError, err, "")
return
}
_, err = global.LoadPolicy(c)
if err != nil {
e.Error(http.StatusInternalServerError, err, "")
return
}
e.OK(control.GetId(), "删除成功")
}
func (e SysRole) UpdateRoleDataScope(c *gin.Context) {
e.Context = c
log := e.GetLogger()
control := new(dto.RoleDataScopeReq)
db, err := e.GetOrm()
if err != nil {
log.Error(err)
return
}
//更新操作
err = c.Bind(control)
if err != nil {
log.Errorf("request bind error, %s", err.Error())
e.Error(http.StatusUnprocessableEntity, err, "参数验证失败")
return
}
data := &system.SysRole{
RoleId: control.RoleId,
DataScope: control.DataScope,
DeptIds: control.DeptIds,
}
data.UpdateBy = user.GetUserId(c)
s := &service.SysRole{}
s.Orm = db
s.Log = log
err = s.UpdateDataScope(data)
if err != nil {
e.Error(http.StatusInternalServerError, err, "")
return
}
e.OK(nil, "操作成功")
}
-582
View File
@@ -1,582 +0,0 @@
package tools
import (
"bytes"
"net/http"
"strconv"
"text/template"
"time"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/api"
"github.com/go-admin-team/go-admin-core/sdk/config"
"github.com/go-admin-team/go-admin-core/sdk/pkg"
"go-admin/app/admin/models"
"go-admin/app/admin/models/tools"
"go-admin/common/apis"
)
type Gen struct {
apis.Api
}
func (e Gen) Preview(c *gin.Context) {
e.Context = c
log := e.GetLogger()
table := tools.SysTables{}
id, err := pkg.StringToInt(c.Param("tableId"))
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
table.TableId = id
t1, err := template.ParseFiles("template/v4/model.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
t2, err := template.ParseFiles("template/v4/no_actions/apis.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
t3, err := template.ParseFiles("template/v4/js.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
t4, err := template.ParseFiles("template/v4/vue.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
t5, err := template.ParseFiles("template/v4/no_actions/router_check_role.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
t6, err := template.ParseFiles("template/v4/dto.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
t7, err := template.ParseFiles("template/v4/no_actions/service.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
db, err := pkg.GetOrm(c)
if err != nil {
log.Errorf("get db connection error, %s", err.Error())
e.Error(http.StatusInternalServerError, err, "数据库连接获取失败")
return
}
tab, _ := table.Get(db)
var b1 bytes.Buffer
err = t1.Execute(&b1, tab)
var b2 bytes.Buffer
err = t2.Execute(&b2, tab)
var b3 bytes.Buffer
err = t3.Execute(&b3, tab)
var b4 bytes.Buffer
err = t4.Execute(&b4, tab)
var b5 bytes.Buffer
err = t5.Execute(&b5, tab)
var b6 bytes.Buffer
err = t6.Execute(&b6, tab)
var b7 bytes.Buffer
err = t7.Execute(&b7, tab)
mp := make(map[string]interface{})
mp["template/model.go.template"] = b1.String()
mp["template/api.go.template"] = b2.String()
mp["template/js.go.template"] = b3.String()
mp["template/vue.go.template"] = b4.String()
mp["template/router.go.template"] = b5.String()
mp["template/dto.go.template"] = b6.String()
mp["template/service.go.template"] = b7.String()
e.OK(mp, "")
}
func (e Gen) GenCode(c *gin.Context) {
e.Context = c
log := e.GetLogger()
table := tools.SysTables{}
id, err := pkg.StringToInt(c.Param("tableId"))
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
db, err := pkg.GetOrm(c)
if err != nil {
log.Errorf("get db connection error, %s", err.Error())
e.Error(http.StatusInternalServerError, err, "数据库连接获取失败")
return
}
table.TableId = id
tab, _ := table.Get(db)
if tab.IsActions == 1 {
e.ActionsGen(c, tab)
} else {
e.NOActionsGen(c, tab)
}
e.OK("", "Code generated successfully")
}
func (e Gen) GenApiToFile(c *gin.Context) {
e.Context = c
log := e.GetLogger()
table := tools.SysTables{}
id, err := pkg.StringToInt(c.Param("tableId"))
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
db, err := pkg.GetOrm(c)
if err != nil {
log.Errorf("get db connection error, %s", err.Error())
e.Error(http.StatusInternalServerError, err, "数据库连接获取失败")
return
}
table.TableId = id
tab, _ := table.Get(db)
e.genApiToFile(c, tab)
e.OK("", "Code generated successfully")
}
func (e Gen) NOActionsGen(c *gin.Context, tab tools.SysTables) {
e.Context = c
log := e.GetLogger()
basePath := "template/v4/"
routerFile := basePath + "no_actions/router_check_role.go.template"
if tab.IsAuth == 2 {
routerFile = basePath + "no_actions/router_no_check_role.go.template"
}
t1, err := template.ParseFiles(basePath + "model.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
t2, err := template.ParseFiles(basePath + "no_actions/apis.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
t3, err := template.ParseFiles(routerFile)
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
t4, err := template.ParseFiles(basePath + "js.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
t5, err := template.ParseFiles(basePath + "vue.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
t6, err := template.ParseFiles(basePath + "dto.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
t7, err := template.ParseFiles(basePath + "no_actions/service.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
_ = pkg.PathCreate("./app/" + tab.PackageName + "/apis/" + tab.ModuleName)
_ = pkg.PathCreate("./app/" + tab.PackageName + "/models/")
_ = pkg.PathCreate("./app/" + tab.PackageName + "/router/")
_ = pkg.PathCreate("./app/" + tab.PackageName + "/service/dto/")
_ = pkg.PathCreate(config.GenConfig.FrontPath + "/api/")
_ = pkg.PathCreate(config.GenConfig.FrontPath + "/views/" + tab.BusinessName)
var b1 bytes.Buffer
err = t1.Execute(&b1, tab)
var b2 bytes.Buffer
err = t2.Execute(&b2, tab)
var b3 bytes.Buffer
err = t3.Execute(&b3, tab)
var b4 bytes.Buffer
err = t4.Execute(&b4, tab)
var b5 bytes.Buffer
err = t5.Execute(&b5, tab)
var b6 bytes.Buffer
err = t6.Execute(&b6, tab)
var b7 bytes.Buffer
err = t7.Execute(&b7, tab)
pkg.FileCreate(b1, "./app/"+tab.PackageName+"/models/"+tab.BusinessName+".go")
pkg.FileCreate(b2, "./app/"+tab.PackageName+"/apis/"+tab.ModuleName+"/"+tab.BusinessName+".go")
pkg.FileCreate(b3, "./app/"+tab.PackageName+"/router/"+tab.BusinessName+".go")
pkg.FileCreate(b4, config.GenConfig.FrontPath+"/api/"+tab.BusinessName+".js")
pkg.FileCreate(b5, config.GenConfig.FrontPath+"/views/"+tab.BusinessName+"/index.vue")
pkg.FileCreate(b6, "./app/"+tab.PackageName+"/service/dto/"+tab.BusinessName+".go")
pkg.FileCreate(b7, "./app/"+tab.PackageName+"/service/"+tab.BusinessName+".go")
}
func (e Gen) genApiToFile(c *gin.Context, tab tools.SysTables) {
e.Context = c
log := e.GetLogger()
basePath := "template/"
t1, err := template.ParseFiles(basePath + "api_migrate.template")
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
i := strconv.FormatInt(time.Now().UnixNano()/1e6, 10)
var b1 bytes.Buffer
err = t1.Execute(&b1, struct {
tools.SysTables
GenerateTime string
}{tab, i})
pkg.FileCreate(b1, "./cmd/migrate/migration/version-local/"+i+"_migrate.go")
}
func (e Gen) ActionsGen(c *gin.Context, tab tools.SysTables) {
e.Context = c
log := api.GetRequestLogger(c)
basePath := "template/v4/"
routerFile := basePath + "actions/router_check_role.go.template"
if tab.IsAuth == 2 {
routerFile = basePath + "actions/router_no_check_role.go.template"
}
t1, err := template.ParseFiles(basePath + "model.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
t3, err := template.ParseFiles(routerFile)
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
t4, err := template.ParseFiles(basePath + "js.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
t5, err := template.ParseFiles(basePath + "vue.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
t6, err := template.ParseFiles(basePath + "dto.go.template")
if err != nil {
log.Error(err)
e.Error(500, err, "")
return
}
_ = pkg.PathCreate("./app/" + tab.PackageName + "/models/")
_ = pkg.PathCreate("./app/" + tab.PackageName + "/router/")
_ = pkg.PathCreate("./app/" + tab.PackageName + "/service/dto/")
_ = pkg.PathCreate(config.GenConfig.FrontPath + "/api/")
_ = pkg.PathCreate(config.GenConfig.FrontPath + "/views/" + tab.BusinessName)
var b1 bytes.Buffer
err = t1.Execute(&b1, tab)
var b3 bytes.Buffer
err = t3.Execute(&b3, tab)
var b4 bytes.Buffer
err = t4.Execute(&b4, tab)
var b5 bytes.Buffer
err = t5.Execute(&b5, tab)
var b6 bytes.Buffer
err = t6.Execute(&b6, tab)
pkg.FileCreate(b1, "./app/"+tab.PackageName+"/models/"+tab.BusinessName+".go")
pkg.FileCreate(b3, "./app/"+tab.PackageName+"/router/"+tab.BusinessName+".go")
pkg.FileCreate(b4, config.GenConfig.FrontPath+"/api/"+tab.BusinessName+".js")
pkg.FileCreate(b5, config.GenConfig.FrontPath+"/views/"+tab.BusinessName+"/index.vue")
pkg.FileCreate(b6, "./app/"+tab.PackageName+"/service/dto/"+tab.BusinessName+".go")
}
func (e Gen) GenMenuAndApi(c *gin.Context) {
log := api.GetRequestLogger(c)
e.Context = c
table := tools.SysTables{}
timeNow := pkg.GetCurrentTime()
id, err := pkg.StringToInt(c.Param("tableId"))
pkg.HasError(err, "", -1)
db, err := pkg.GetOrm(c)
if err != nil {
log.Errorf("get db connection error, %s", err.Error())
e.Error(http.StatusInternalServerError, err, "数据库连接获取失败")
return
}
table.TableId = id
tab, _ := table.Get(db)
Mmenu := models.Menu{}
Mmenu.MenuName = tab.TBName + "Manage"
Mmenu.Title = tab.TableComment
Mmenu.Icon = "pass"
Mmenu.Path = "/" + tab.TBName
Mmenu.MenuType = "M"
Mmenu.Action = "无"
Mmenu.ParentId = 0
Mmenu.NoCache = false
Mmenu.Component = "Layout"
Mmenu.Sort = 0
Mmenu.Visible = "0"
Mmenu.IsFrame = "0"
Mmenu.CreateBy = "1"
Mmenu.UpdateBy = "1"
Mmenu.CreatedAt = timeNow
Mmenu.UpdatedAt = timeNow
Mmenu.MenuId, err = Mmenu.Create(db)
Cmenu := models.Menu{}
Cmenu.MenuName = tab.TBName
Cmenu.Title = tab.TableComment
Cmenu.Icon = "pass"
Cmenu.Path = tab.TBName
Cmenu.MenuType = "C"
Cmenu.Action = "无"
Cmenu.Permission = tab.PackageName + ":" + tab.BusinessName + ":list"
Cmenu.ParentId = Mmenu.MenuId
Cmenu.NoCache = false
Cmenu.Component = "/" + tab.BusinessName + "/index"
Cmenu.Sort = 0
Cmenu.Visible = "0"
Cmenu.IsFrame = "0"
Cmenu.CreateBy = "1"
Cmenu.UpdateBy = "1"
Cmenu.CreatedAt = timeNow
Cmenu.UpdatedAt = timeNow
Cmenu.MenuId, err = Cmenu.Create(db)
MList := models.Menu{}
MList.MenuName = ""
MList.Title = "分页获取" + tab.TableComment
MList.Icon = ""
MList.Path = tab.TBName
MList.MenuType = "F"
MList.Action = "无"
MList.Permission = tab.PackageName + ":" + tab.BusinessName + ":query"
MList.ParentId = Cmenu.MenuId
MList.NoCache = false
MList.Sort = 0
MList.Visible = "0"
MList.IsFrame = "0"
MList.CreateBy = "1"
MList.UpdateBy = "1"
MList.CreatedAt = timeNow
MList.UpdatedAt = timeNow
MList.MenuId, err = MList.Create(db)
MCreate := models.Menu{}
MCreate.MenuName = ""
MCreate.Title = "创建" + tab.TableComment
MCreate.Icon = ""
MCreate.Path = tab.TBName
MCreate.MenuType = "F"
MCreate.Action = "无"
MCreate.Permission = tab.PackageName + ":" + tab.BusinessName + ":add"
MCreate.ParentId = Cmenu.MenuId
MCreate.NoCache = false
MCreate.Sort = 0
MCreate.Visible = "0"
MCreate.IsFrame = "0"
MCreate.CreateBy = "1"
MCreate.UpdateBy = "1"
MCreate.CreatedAt = timeNow
MCreate.UpdatedAt = timeNow
MCreate.MenuId, err = MCreate.Create(db)
MUpdate := models.Menu{}
MUpdate.MenuName = ""
MUpdate.Title = "修改" + tab.TableComment
MUpdate.Icon = ""
MUpdate.Path = tab.TBName
MUpdate.MenuType = "F"
MUpdate.Action = "无"
MUpdate.Permission = tab.PackageName + ":" + tab.BusinessName + ":edit"
MUpdate.ParentId = Cmenu.MenuId
MUpdate.NoCache = false
MUpdate.Sort = 0
MUpdate.Visible = "0"
MUpdate.IsFrame = "0"
MUpdate.CreateBy = "1"
MUpdate.UpdateBy = "1"
MUpdate.CreatedAt = timeNow
MUpdate.UpdatedAt = timeNow
MUpdate.MenuId, err = MUpdate.Create(db)
MDelete := models.Menu{}
MDelete.MenuName = ""
MDelete.Title = "删除" + tab.TableComment
MDelete.Icon = ""
MDelete.Path = tab.TBName
MDelete.MenuType = "F"
MDelete.Action = "无"
MDelete.Permission = tab.PackageName + ":" + tab.BusinessName + ":remove"
MDelete.ParentId = Cmenu.MenuId
MDelete.NoCache = false
MDelete.Sort = 0
MDelete.Visible = "0"
MDelete.IsFrame = "0"
MDelete.CreateBy = "1"
MDelete.UpdateBy = "1"
MDelete.CreatedAt = timeNow
MDelete.UpdatedAt = timeNow
MDelete.MenuId, err = MDelete.Create(db)
var InterfaceId = 63
Amenu := models.Menu{}
Amenu.MenuName = tab.TBName
Amenu.Title = tab.TableComment
Amenu.Icon = "bug"
Amenu.Path = tab.TBName
Amenu.MenuType = "M"
Amenu.Action = "无"
Amenu.ParentId = InterfaceId
Amenu.NoCache = false
Amenu.Sort = 0
Amenu.Visible = "1"
Amenu.IsFrame = "0"
Amenu.CreateBy = "1"
Amenu.UpdateBy = "1"
Amenu.CreatedAt = timeNow
Amenu.UpdatedAt = timeNow
Amenu.MenuId, err = Amenu.Create(db)
AList := models.Menu{}
AList.MenuName = ""
AList.Title = "分页获取" + tab.TableComment
AList.Icon = "bug"
AList.Path = "/api/v1/" + tab.ModuleName
AList.MenuType = "A"
AList.Action = "GET"
AList.ParentId = Amenu.MenuId
AList.NoCache = false
AList.Sort = 0
AList.Visible = "1"
AList.IsFrame = "0"
AList.CreateBy = "1"
AList.UpdateBy = "1"
AList.CreatedAt = timeNow
AList.UpdatedAt = timeNow
AList.MenuId, err = AList.Create(db)
AGet := models.Menu{}
AGet.MenuName = ""
AGet.Title = "根据id获取" + tab.TableComment
AGet.Icon = "bug"
AGet.Path = "/api/v1/" + tab.ModuleName + "/:id"
AGet.MenuType = "A"
AGet.Action = "GET"
AGet.ParentId = Amenu.MenuId
AGet.NoCache = false
AGet.Sort = 0
AGet.Visible = "1"
AGet.IsFrame = "0"
AGet.CreateBy = "1"
AGet.UpdateBy = "1"
AGet.CreatedAt = timeNow
AGet.UpdatedAt = timeNow
AGet.MenuId, err = AGet.Create(db)
ACreate := models.Menu{}
ACreate.MenuName = ""
ACreate.Title = "创建" + tab.TableComment
ACreate.Icon = "bug"
ACreate.Path = "/api/v1/" + tab.ModuleName
ACreate.MenuType = "A"
ACreate.Action = "POST"
ACreate.ParentId = Amenu.MenuId
ACreate.NoCache = false
ACreate.Sort = 0
ACreate.Visible = "1"
ACreate.IsFrame = "0"
ACreate.CreateBy = "1"
ACreate.UpdateBy = "1"
ACreate.CreatedAt = timeNow
ACreate.UpdatedAt = timeNow
ACreate.MenuId, err = ACreate.Create(db)
AUpdate := models.Menu{}
AUpdate.MenuName = ""
AUpdate.Title = "修改" + tab.TableComment
AUpdate.Icon = "bug"
AUpdate.Path = "/api/v1/" + tab.ModuleName + "/:id"
AUpdate.MenuType = "A"
AUpdate.Action = "PUT"
AUpdate.ParentId = Amenu.MenuId
AUpdate.NoCache = false
AUpdate.Sort = 0
AUpdate.Visible = "1"
AUpdate.IsFrame = "0"
AUpdate.CreateBy = "1"
AUpdate.UpdateBy = "1"
AUpdate.CreatedAt = timeNow
AUpdate.UpdatedAt = timeNow
AUpdate.MenuId, err = AUpdate.Create(db)
ADelete := models.Menu{}
ADelete.MenuName = ""
ADelete.Title = "删除" + tab.TableComment
ADelete.Icon = "bug"
ADelete.Path = "/api/v1/" + tab.ModuleName
ADelete.MenuType = "A"
ADelete.Action = "DELETE"
ADelete.ParentId = Amenu.MenuId
ADelete.NoCache = false
ADelete.Sort = 0
ADelete.Visible = "1"
ADelete.IsFrame = "0"
ADelete.CreateBy = "1"
ADelete.UpdateBy = "1"
ADelete.CreatedAt = timeNow
ADelete.UpdatedAt = timeNow
ADelete.MenuId, err = ADelete.Create(db)
e.OK("", "数据生成成功!")
}
+16
View File
@@ -0,0 +1,16 @@
package models
type CasbinRule struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Ptype string `gorm:"size:512;uniqueIndex:unique_index"`
V0 string `gorm:"size:512;uniqueIndex:unique_index"`
V1 string `gorm:"size:512;uniqueIndex:unique_index"`
V2 string `gorm:"size:512;uniqueIndex:unique_index"`
V3 string `gorm:"size:512;uniqueIndex:unique_index"`
V4 string `gorm:"size:512;uniqueIndex:unique_index"`
V5 string `gorm:"size:512;uniqueIndex:unique_index"`
}
func (CasbinRule) TableName() string {
return "sys_casbin_rule"
}
+1 -1
View File
@@ -31,7 +31,7 @@ func ExecSql(db *gorm.DB, filePath string) error {
fmt.Println(sqlList[i]) fmt.Println(sqlList[i])
continue continue
} }
sql := strings.Replace(sqlList[i]+";", "\n", "", 0) sql := strings.Replace(sqlList[i]+";", "\n", "", -1)
sql = strings.TrimSpace(sql) sql = strings.TrimSpace(sql)
if err = db.Exec(sql).Error; err != nil { if err = db.Exec(sql).Error; err != nil {
log.Printf("error sql: %s", sql) log.Printf("error sql: %s", sql)
-343
View File
@@ -1,343 +0,0 @@
package models
import (
"errors"
"github.com/go-admin-team/go-admin-core/sdk/pkg"
"go-admin/common/models"
"gorm.io/gorm"
"go-admin/app/admin/models/system"
)
type Menu struct {
MenuId int `json:"menuId" gorm:"primaryKey;autoIncrement"`
MenuName string `json:"menuName" gorm:"size:128;"`
Title string `json:"title" gorm:"size:128;"`
Icon string `json:"icon" gorm:"size:128;"`
Path string `json:"path" gorm:"size:128;"`
Paths string `json:"paths" gorm:"size:128;"`
MenuType string `json:"menuType" gorm:"size:1;"`
Action string `json:"action" gorm:"size:16;"`
Permission string `json:"permission" gorm:"size:255;"`
ParentId int `json:"parentId" gorm:"size:11;"`
NoCache bool `json:"noCache" gorm:"size:8;"`
Breadcrumb string `json:"breadcrumb" gorm:"size:255;"`
Component string `json:"component" gorm:"size:255;"`
Sort int `json:"sort" gorm:"size:4;"`
Visible string `json:"visible" gorm:"size:1;"`
CreateBy string `json:"createBy" gorm:"size:128;"`
UpdateBy string `json:"updateBy" gorm:"size:128;"`
IsFrame string `json:"isFrame" gorm:"size:1;DEFAULT:0;"`
DataScope string `json:"dataScope" gorm:"-"`
Params string `json:"params" gorm:"-"`
RoleId int `gorm:"-"`
Children []Menu `json:"children" gorm:"-"`
IsSelect bool `json:"is_select" gorm:"-"`
models.ModelTime
}
func (Menu) TableName() string {
return "sys_menu"
}
type MenuLable struct {
Id int `json:"id" gorm:"-"`
Label string `json:"label" gorm:"-"`
Children []MenuLable `json:"children" gorm:"-"`
}
type Menus struct {
MenuId int `json:"menuId" gorm:"column:menu_id;primaryKey;autoIncrement;"`
MenuName string `json:"menuName" gorm:"column:menu_name"`
Title string `json:"title" gorm:"column:title"`
Icon string `json:"icon" gorm:"column:icon"`
Path string `json:"path" gorm:"column:path"`
MenuType string `json:"menuType" gorm:"column:menu_type"`
Action string `json:"action" gorm:"column:action"`
Permission string `json:"permission" gorm:"column:permission"`
ParentId int `json:"parentId" gorm:"column:parent_id"`
NoCache bool `json:"noCache" gorm:"column:no_cache"`
Breadcrumb string `json:"breadcrumb" gorm:"column:breadcrumb"`
Component string `json:"component" gorm:"column:component"`
Sort int `json:"sort" gorm:"column:sort"`
Visible string `json:"visible" gorm:"column:visible"`
Children []Menu `json:"children" gorm:"-"`
CreateBy string `json:"createBy" gorm:"column:create_by"`
UpdateBy string `json:"updateBy" gorm:"column:update_by"`
DataScope string `json:"dataScope" gorm:"-"`
Params string `json:"params" gorm:"-"`
BaseModel
}
func (Menus) TableName() string {
return "sys_menu"
}
type MenuRole struct {
Menus
IsSelect bool `json:"is_select" gorm:"-"`
}
type MS []Menu
//func (e *Menu) GetByMenuId() (Menu Menu, err error) {
//
// table := orm.Eloquent.Table(e.TableName())
// table = table.Where("menu_id = ?", e.MenuId)
// if err = table.Find(&Menu).Error; err != nil {
// return
// }
// return
//}
//func (e *Menu) SetMenu() (m []Menu, err error) {
// menulist, err := e.GetPage()
//
// m = make([]Menu, 0)
// for i := 0; i < len(menulist); i++ {
// if menulist[i].ParentId != 0 {
// continue
// }
// menusInfo := DiguiMenu(&menulist, menulist[i])
//
// m = append(m, menusInfo)
// }
// return
//}
//func DiguiMenu(menulist *[]Menu, menu Menu) Menu {
// list := *menulist
//
// min := make([]Menu, 0)
// for j := 0; j < len(list); j++ {
//
// if menu.MenuId != list[j].ParentId {
// continue
// }
// mi := Menu{}
// mi.MenuId = list[j].MenuId
// mi.MenuName = list[j].MenuName
// mi.Title = list[j].Title
// mi.Icon = list[j].Icon
// mi.Path = list[j].Path
// mi.MenuType = list[j].MenuType
// mi.Action = list[j].Action
// mi.Permission = list[j].Permission
// mi.ParentId = list[j].ParentId
// mi.NoCache = list[j].NoCache
// mi.Breadcrumb = list[j].Breadcrumb
// mi.Component = list[j].Component
// mi.Sort = list[j].Sort
// mi.Visible = list[j].Visible
// mi.CreatedAt = list[j].CreatedAt
// mi.Children = []Menu{}
//
// if mi.MenuType != "F" {
// ms := DiguiMenu(menulist, mi)
// min = append(min, ms)
//
// } else {
// min = append(min, mi)
// }
//
// }
// menu.Children = min
// return menu
//}
//func (e *Menu) SetMenuLabel() (m *[]MenuLable, err error) {
// menulist, err := e.Get()
//
// ml := make([]MenuLable, 0)
// for i := 0; i < len(menulist); i++ {
// if menulist[i].ParentId != 0 {
// continue
// }
// e := MenuLable{}
// e.Id = menulist[i].MenuId
// e.Label = menulist[i].Title
// menusInfo := MenuLabelCall(&menulist, e)
//
// ml = append(ml, menusInfo)
// }
// return &ml, err
//}
//func MenuLabelCall(menulist *[]Menu, menu MenuLable) MenuLable {
// list := *menulist
//
// min := make([]MenuLable, 0)
// for j := 0; j < len(list); j++ {
//
// if menu.Id != list[j].ParentId {
// continue
// }
// mi := MenuLable{}
// mi.Id = list[j].MenuId
// mi.Label = list[j].Title
// mi.Children = []MenuLable{}
// if list[j].MenuType != "F" {
// ms := MenuLabelCall(menulist, mi)
// min = append(min, ms)
// } else {
// min = append(min, mi)
// }
//
// }
// if len(min) > 0 {
// menu.Children = min
// } else {
// menu.Children = nil
// }
// return menu
//}
//func (e *Menu) SetMenuRole(roleName string) (m []Menu, err error) {
//
// menus, err := e.GetByRoleName(roleName)
//
// m = make([]Menu, 0)
// for i := 0; i < len(menus); i++ {
// if menus[i].ParentId != 0 {
// continue
// }
// menusInfo := DiguiMenu(&menus, menus[i])
//
// m = append(m, menusInfo)
// }
// return
//}
//func (e *MenuRole) Get(tx *gorm.DB) (Menus []MenuRole, err error) {
// table := tx.Table(e.TableName())
// if e.MenuName != "" {
// table = table.Where("menu_name = ?", e.MenuName)
// }
// if err = table.Order("sort").Find(&Menus).Error; err != nil {
// return
// }
// return
//}
//func (e *Menu) GetByRoleName(roleName string) (Menus []Menu, err error) {
// var table *gorm.DB
// if roleName == "admin" {
// table = orm.Eloquent.Table(e.TableName()).Select("sys_menu.*")
// table = table.Where(" menu_type in ('M','C')")
// } else {
// table = orm.Eloquent.Table(e.TableName()).Select("sys_menu.*").Joins("left join sys_role_menu on sys_role_menu.menu_id=sys_menu.menu_id")
// table = table.Where("sys_role_menu.role_name=? and menu_type in ('M','C')", roleName)
// }
// if err = table.Order("sort").Find(&Menus).Error; err != nil {
// return
// }
// return
//}
func (e *Menu) Get(tx *gorm.DB) (Menus []Menu, err error) {
table := tx.Table(e.TableName())
if e.MenuName != "" {
table = table.Where("menu_name = ?", e.MenuName)
}
if e.Path != "" {
table = table.Where("path = ?", e.Path)
}
if e.Action != "" {
table = table.Where("action = ?", e.Action)
}
if e.MenuType != "" {
table = table.Where("menu_type = ?", e.MenuType)
}
if err = table.Order("sort").Find(&Menus).Error; err != nil {
return
}
return
}
func (e *Menu) GetPage(tx *gorm.DB) (Menus []Menu, err error) {
table := tx.Table(e.TableName())
if e.MenuName != "" {
table = table.Where("menu_name = ?", e.MenuName)
}
if e.Title != "" {
table = table.Where("title = ?", e.Title)
}
if e.Visible != "" {
table = table.Where("visible = ?", e.Visible)
}
if e.MenuType != "" {
table = table.Where("menu_type = ?", e.MenuType)
}
// 数据权限控制
dataPermission := new(system.DataPermission)
dataPermission.UserId, _ = pkg.StringToInt(e.DataScope)
table, err = dataPermission.GetDataScope("sys_menu", table)
if err != nil {
return nil, err
}
if err = table.Order("sort").Find(&Menus).Error; err != nil {
return
}
return
}
func (e *Menu) Create(tx *gorm.DB) (id int, err error) {
result := tx.Table(e.TableName()).Create(&e)
if result.Error != nil {
err = result.Error
return
}
err = InitPaths(tx, e)
if err != nil {
return
}
id = e.MenuId
return
}
func InitPaths(tx *gorm.DB, menu *Menu) (err error) {
parentMenu := new(Menu)
if menu.ParentId != 0 {
tx.Table("sys_menu").Where("menu_id = ?", menu.ParentId).First(parentMenu)
if parentMenu.Paths == "" {
err = errors.New("父级paths异常,请尝试对当前节点父级菜单进行更新操作!")
return
}
menu.Paths = parentMenu.Paths + "/" + pkg.IntToString(menu.MenuId)
} else {
menu.Paths = "/0/" + pkg.IntToString(menu.MenuId)
}
tx.Table("sys_menu").Where("menu_id = ?", menu.MenuId).Update("paths", menu.Paths)
return
}
//func (e *Menu) Update(tx *gorm.DB, id int) (update Menu, err error) {
// if err = tx.Table(e.TableName()).First(&update, id).Error; err != nil {
// return
// }
//
// //参数1:是要修改的数据
// //参数2:是修改的数据
// if err = tx.Table(e.TableName()).Model(&update).Updates(&e).Error; err != nil {
// return
// }
// err = InitPaths(tx, e)
// if err != nil {
// return
// }
// return
//}
//func (e *Menu) Delete(tx *gorm.DB, id int) (success bool, err error) {
// if err = tx.Table(e.TableName()).Where("menu_id = ?", id).Delete(&Menu{}).Error; err != nil {
// success = false
// return
// }
// success = true
// return
//}
-11
View File
@@ -1,11 +0,0 @@
package models
import (
"time"
)
type BaseModel struct {
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt *time.Time `json:"deletedAt"`
}
-243
View File
@@ -1,243 +0,0 @@
package models
import "go-admin/common/models"
type SysRole struct {
RoleId int `json:"roleId" gorm:"primaryKey;autoIncrement"` // 角色编码
RoleName string `json:"roleName" gorm:"size:128;"` // 角色名称
Status string `json:"status" gorm:"size:4;"` //
RoleKey string `json:"roleKey" gorm:"size:128;"` //角色代码
RoleSort int `json:"roleSort" gorm:""` //角色排序
Flag string `json:"flag" gorm:"size:128;"` //
Remark string `json:"remark" gorm:"size:255;"` //备注
Admin bool `json:"admin" gorm:"size:4;"`
DataScope string `json:"dataScope" gorm:"size:128;"`
models.ModelTime
models.ControlBy
Params string `json:"params" gorm:"-"`
MenuIds []int `json:"menuIds" gorm:"-"`
DeptIds []int `json:"deptIds" gorm:"-"`
}
func (SysRole) TableName() string {
return "sys_role"
}
type MenuIdList struct {
MenuId int `json:"menuId"`
}
//func (role *SysRole) GetById(tx *gorm.DB, id interface{}) error {
// return tx.First(role, id).Error
//}
//
//func (role *SysRole) GetPage(pageSize int, pageIndex int) ([]SysRole, int, error) {
// var doc []SysRole
//
// table := orm.Eloquent.Table("sys_role")
// if role.RoleId != 0 {
// table = table.Where("role_id = ?", role.RoleId)
// }
// if role.RoleName != "" {
// table = table.Where("role_name = ?", role.RoleName)
// }
// if role.Status != "" {
// table = table.Where("status = ?", role.Status)
// }
// if role.RoleKey != "" {
// table = table.Where("role_key = ?", role.RoleKey)
// }
//
// // 数据权限控制
// dataPermission := new(DataPermission)
// dataPermission.UserId, _ = tools.StringToInt(role.DataScope)
// table, err := dataPermission.GetDataScope("sys_role", table)
// if err != nil {
// return nil, 0, err
// }
// var count int64
//
// if err := table.Order("role_sort").Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&doc).Offset(-1).Limit(-1).Count(&count).Error; err != nil {
// return nil, 0, err
// }
// //table.Where("`deleted_at` IS NULL").Count(&count)
// return doc, int(count), nil
//}
//
//func (role *SysRole) Get() (SysRole SysRole, err error) {
// table := orm.Eloquent.Table("sys_role")
// if role.RoleId != 0 {
// table = table.Where("role_id = ?", role.RoleId)
// }
// if role.RoleName != "" {
// table = table.Where("role_name = ?", role.RoleName)
// }
// if err = table.First(&SysRole).Error; err != nil {
// return
// }
//
// return
//}
//
//func (role *SysRole) GetOne(sysRole *SysRole) (err error) {
// table := orm.Eloquent.Table("sys_role")
// if role.RoleId != 0 {
// table = table.Where("role_id = ?", role.RoleId)
// }
// if role.RoleName != "" {
// table = table.Where("role_name = ?", role.RoleName)
// }
// if err = table.First(sysRole).Error; err != nil {
// return
// }
//
// return
//}
//
//func (role *SysRole) GetList() (SysRole []SysRole, err error) {
// table := orm.Eloquent.Table("sys_role")
// if role.RoleId != 0 {
// table = table.Where("role_id = ?", role.RoleId)
// }
// if role.RoleName != "" {
// table = table.Where("role_name = ?", role.RoleName)
// }
// if err = table.Order("role_sort").Find(&SysRole).Error; err != nil {
// return
// }
//
// return
//}
//
//// 获取角色对应的菜单ids
//func (role *SysRole) GetRoleMeunId() ([]int, error) {
// menuIds := make([]int, 0)
// menuList := make([]MenuIdList, 0)
// if err := orm.Eloquent.Table("sys_role_menu").
// Select("sys_role_menu.menu_id").
// Where("role_id = ? ", role.RoleId).
// Where(" sys_role_menu.menu_id not in(select sys_menu.parent_id from sys_role_menu " +
// "LEFT JOIN sys_menu on sys_menu.menu_id=sys_role_menu.menu_id where role_id =? and parent_id is not null)", role.RoleId).
// Find(&menuList).Error; err != nil {
// return nil, err
// }
//
// for i := 0; i < len(menuList); i++ {
// menuIds = append(menuIds, menuList[i].MenuId)
// }
// return menuIds, nil
//}
//
//func (role *SysRole) Insert() (id int, err error) {
// var i int64
// orm.Eloquent.Table(role.TableName()).Where("role_name=? or role_key = ?", role.RoleName, role.RoleKey).Count(&i)
// if i > 0 {
// return 0, errors.New("角色名称或者角色标识已经存在!")
// }
// role.UpdateBy = ""
// result := orm.Eloquent.Table(role.TableName()).Create(&role)
// if result.Error != nil {
// err = result.Error
// return
// }
// id = role.RoleId
// return
//}
//
//type DeptIdList struct {
// DeptId int `json:"DeptId"`
//}
//
//func (role *SysRole) GetRoleDeptId() ([]int, error) {
// deptIds := make([]int, 0)
// deptList := make([]DeptIdList, 0)
// if err := orm.Eloquent.Table("sys_role_dept").Select("sys_role_dept.dept_id").Joins("LEFT JOIN sys_dept on sys_dept.dept_id=sys_role_dept.dept_id").Where("role_id = ? ", role.RoleId).Where(" sys_role_dept.dept_id not in(select sys_dept.parent_id from sys_role_dept LEFT JOIN sys_dept on sys_dept.dept_id=sys_role_dept.dept_id where role_id =? )", role.RoleId).Find(&deptList).Error; err != nil {
// return nil, err
// }
//
// for i := 0; i < len(deptList); i++ {
// deptIds = append(deptIds, deptList[i].DeptId)
// }
//
// return deptIds, nil
//}
//
////修改
//func (role *SysRole) Update(id int) (update SysRole, err error) {
// if err = orm.Eloquent.Table(role.TableName()).First(&update, id).Error; err != nil {
// return
// }
//
// if role.RoleName != "" && role.RoleName != update.RoleName {
// return update, errors.New("角色名称不允许修改!")
// }
//
// if role.RoleKey != "" && role.RoleKey != update.RoleKey {
// return update, errors.New("角色标识不允许修改!")
// }
//
// //参数1:是要修改的数据
// //参数2:是修改的数据
// if err = orm.Eloquent.Table(role.TableName()).Model(&update).Updates(&role).Error; err != nil {
// return
// }
// return
//}
//
////批量删除
//func (role *SysRole) BatchDelete(id []int) (Result bool, err error) {
// tx := orm.Eloquent.Begin()
//
// defer func() {
// if r := recover(); r != nil {
// tx.Rollback()
// }
// }()
//
// if err := tx.Error; err != nil {
// return false, err
// }
// // 查询角色
// var roles []SysRole
// if err := tx.Table("sys_role").Where("role_id in (?)", id).Find(&roles).Error; err != nil {
// tx.Rollback()
// return false, err
// }
//
// var count int64
// if err := tx.Table("sys_user").Where("role_id in (?)", id).Count(&count).Error; err != nil {
// tx.Rollback()
// return false, err
// }
// if count > 0 {
// tx.Rollback()
// return false, errors.New("存在绑定用户,请解绑后重试")
// }
//
// // 删除角色
// if err = tx.Table(role.TableName()).Where("role_id in (?)", id).Unscoped().Delete(&SysRole{}).Error; err != nil {
// tx.Rollback()
// return false, err
// }
//
// // 删除角色菜单
// if err := tx.Table("sys_role_menu").Where("role_id in (?)", id).Delete(&RoleMenu{}).Error; err != nil {
// tx.Rollback()
// return false, err
// }
//
// // 删除casbin配置
// for i := 0; i < len(roles); i++ {
// if err := tx.Table("sys_casbin_rule").Where("v0 in (?)", roles[0].RoleKey).Delete(&CasbinRule{}).Error; err != nil {
// tx.Rollback()
// return false, err
// }
// }
//
// if err := tx.Commit().Error; err != nil {
// return false, err
// }
//
// return true, nil
//}
+95
View File
@@ -0,0 +1,95 @@
package models
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"regexp"
"strings"
"github.com/bitly/go-simplejson"
"github.com/go-admin-team/go-admin-core/v2/sdk"
"github.com/go-admin-team/go-admin-core/v2/sdk/runtime"
"github.com/go-admin-team/go-admin-core/v2/storage"
"go-admin/common/models"
)
type SysApi struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement;comment:主键编码"`
Handle string `json:"handle" gorm:"size:128;comment:handle"`
Title string `json:"title" gorm:"size:128;comment:标题"`
Path string `json:"path" gorm:"size:128;comment:地址"`
Action string `json:"action" gorm:"size:16;comment:请求类型"`
Type string `json:"type" gorm:"size:16;comment:接口类型"`
// AppCode identifies which application's seed.SeedMenus call wrote this
// row; empty for the host's own built-in APIs. Same NOT NULL DEFAULT ''
// reasoning as SysMenu.AppCode.
AppCode string `json:"appCode" gorm:"type:varchar(64);not null;default:'';index:idx_sys_api_app_code;comment:AppCode"`
models.ModelTime
models.ControlBy
}
func (*SysApi) TableName() string {
return "sys_api"
}
func (e *SysApi) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SysApi) GetId() interface{} {
return e.Id
}
func SaveSysApi(message storage.Messager) (err error) {
var rb []byte
rb, err = json.Marshal(message.GetValues())
if err != nil {
err = fmt.Errorf("json Marshal error, %v", err.Error())
return err
}
var l runtime.Routers
err = json.Unmarshal(rb, &l)
if err != nil {
err = fmt.Errorf("json Unmarshal error, %s", err.Error())
return err
}
dbList := sdk.Runtime.GetAllDb()
for _, d := range dbList {
for _, v := range l.List {
if v.HttpMethod != "HEAD" ||
strings.Contains(v.RelativePath, "/swagger/") ||
strings.Contains(v.RelativePath, "/static/") ||
strings.Contains(v.RelativePath, "/form-generator/") ||
strings.Contains(v.RelativePath, "/sys/tables") {
// 根据接口方法注释里的@Summary填充接口名称,适用于代码生成器
// 可在此处增加配置路径前缀的if判断,只对代码生成的自建应用进行定向的接口名称填充
jsonFile, _ := ioutil.ReadFile("docs/swagger.json")
jsonData, _ := simplejson.NewFromReader(bytes.NewReader(jsonFile))
urlPath := v.RelativePath
idPatten := "(.*)/:(\\w+)" // 正则替换,把:id换成{id}
reg, _ := regexp.Compile(idPatten)
if reg.MatchString(urlPath) {
urlPath = reg.ReplaceAllString(v.RelativePath, "${1}/{${2}}") // 把:id换成{id}
}
apiTitle, _ := jsonData.Get("paths").Get(urlPath).Get(strings.ToLower(v.HttpMethod)).Get("summary").String()
err := d.Debug().Where(SysApi{Path: v.RelativePath, Action: v.HttpMethod}).
Attrs(SysApi{Handle: v.Handler, Title: apiTitle}).
FirstOrCreate(&SysApi{}).
//Update("handle", v.Handler).
Error
if err != nil {
err := fmt.Errorf("Models SaveSysApi error: %s \r\n ", err.Error())
return err
}
}
}
}
return nil
}
+106
View File
@@ -0,0 +1,106 @@
package models
import (
"time"
"go-admin/common/models"
)
// The values sys_app.status takes.
//
// Three states rather than a single "installed", because an install that
// stopped partway has to be an observable row rather than the absence of one:
// the versions an app installs are separate migration files, and on MySQL a
// DDL statement commits the transaction around it - taking an outer
// transaction and every savepoint under it with it - so they cannot be
// wrapped in one.
//
// AppInstalling is also what a row reads as after the process was killed
// mid-install, which is why it is not treated as "installed" by anything.
const (
AppInstalling = 1
AppInstalled = 2
AppFailed = 3
)
// SysApp is the sys_app row model: one row per installed application (PRD
// 008 F2). It deliberately does not embed models.ModelTime - see the design
// doc (docs-prd/008-应用清单与安装器/数据库变更.md) §1.1 for why an
// installed-app registry does not need the millisecond soft-delete marker
// every other sys_* table follows. Uninstalling an app deletes its row
// outright; a later reinstall creates a fresh one.
type SysApp struct {
models.Model // Id int, primary key, autoincrement
// AppCode is the app.Manifest.Code / migration.ForApp / seed.SeedMenus
// identity, already lower-cased by migration.NormalizeAppCode before
// anything reaches this table. Unique: row existence alone answers G2
// ("is app X installed").
AppCode string `json:"appCode" gorm:"type:varchar(64);not null;uniqueIndex:uk_sys_app_app_code;comment:app code"`
Name string `json:"name" gorm:"size:128;not null;comment:display name, from Manifest.Name"`
// Version is the version this row currently reflects - attempted or
// confirmed, disambiguated by Status. It does not drive which
// migrations run next; sys_migration's per-version rows do that (see
// design doc §1.5's resume flow). This field is descriptive, refreshed
// from the manifest on every install/upgrade/resume attempt.
Version string `json:"version" gorm:"size:32;not null;comment:version this row currently reflects, see Status"`
Description string `json:"description" gorm:"size:255;not null;default:'';comment:from Manifest.Description"`
Author string `json:"author" gorm:"size:128;not null;default:'';comment:from Manifest.Author"`
// Requires is a comma-separated list of app codes this app declared as
// dependencies (Manifest.Requires). Stored as plain VARCHAR CSV, not
// JSON - see design doc §1.3 for why. F8 (P1) is what validates and
// orders on this; this batch only stores what the manifest declared.
Requires string `json:"requires" gorm:"size:255;not null;default:'';comment:declared dependency app codes, comma separated"`
// Pricing/License are reserved passthrough fields (PRD 003; PRD 008
// open question 1). This batch stores whatever the manifest carries and
// does not interpret either one.
Pricing string `json:"pricing" gorm:"size:64;not null;default:'';comment:reserved, not interpreted by this batch"`
License string `json:"license" gorm:"size:64;not null;default:'';comment:reserved, not interpreted by this batch"`
// Status: 1=installing 2=installed 3=failed. Three states, not a
// single "1=installed", because a partial, stuck install has to be an
// observable row rather than "the row doesn't exist yet" - see design
// doc §1.5 for why cross-migration-file atomicity is not available on
// MySQL (implicit commit on DDL).
Status int `json:"status" gorm:"size:4;not null;default:1;comment:1=installing 2=installed 3=failed"`
// FailedVersion and LastError are DIAGNOSTIC TEXT ONLY - what a human
// looking at this row is told about the last failure, nothing more. No
// code anywhere may read either one to decide what to do next.
//
// The question "where should a resume pick up" has exactly one
// authoritative answer, and it is not these two columns: subtract
// sys_migration's applied rows for this app_code from what the app's
// own compiled-in code has registered (migration.Snapshot()/ForApp -
// the same set F7's `migrate status` already walks). That answer can
// never go stale, because it is not stored anywhere to go stale - it is
// recomputed from sys_migration every time it is asked. FailedVersion
// is a snapshot of what that computation returned at the moment of
// failure, kept only so an operator does not have to go find the
// process's logs; if it and a fresh recomputation from sys_migration
// ever disagree, sys_migration is right and this column is stale, by
// definition, and nothing should ever notice or care except a human
// reading the row.
FailedVersion string `json:"failedVersion" gorm:"size:64;not null;default:'';comment:diagnostic snapshot only, not a judgment basis; meaningful only when status=3"`
LastError string `json:"lastError" gorm:"size:255;not null;default:'';comment:diagnostic text only, not a judgment basis; meaningful only when status=3"`
// InstalledAt is when this app first reached status=installed - set
// once, never moved by a later upgrade (see design doc §1.4). Nullable,
// unlike every other column here: a row can exist before it has a
// value (a fresh install starts at status=installing). This is not the
// deleted_at problem 1786700003000_soft_delete_marker.go fixed - that
// column sat inside a unique index, where NULL <> NULL let two live
// rows coexist under the same key. InstalledAt is in no index at all,
// so nullability here opens no such hole.
InstalledAt *time.Time `json:"installedAt" gorm:"comment:first successful install time; null until status first reaches installed"`
UpdatedAt time.Time `json:"updatedAt" gorm:"comment:last updated time"`
models.ControlBy // CreateBy/UpdateBy: which operator triggered the attempt
}
func (*SysApp) TableName() string {
return "sys_app"
}
+43
View File
@@ -0,0 +1,43 @@
package models
import "time"
// SysAppCasbinGrant is a ledger of casbin_rule rows an app install created,
// keyed by the exact natural key casbin_rule itself is unique on. It exists
// because casbin_rule is not a table this project owns (see design doc
// docs-prd/008-应用清单与安装器/数据库变更.md §2.2): we cannot add an
// app_code column to it without that column being silently zeroed the first
// time anything calls the gorm-adapter's SavePolicy/SavePolicyCtx. Recording
// the natural key here, instead of a foreign key into casbin_rule, is also
// what survives SysRole.Update's RemoveFilteredPolicy+re-add cycle for a
// role's policies (app/admin/service/sys_role.go): that cycle replaces the
// underlying row (a new auto-increment ID) but reproduces the same
// (ptype,v0,v1,v2) tuple from the same sys_menu/sys_api data, so a
// natural-key match here still finds it. What it does not survive is the
// role being renamed, or the tuple being rebuilt from a completely different
// source (a future SavePolicy call from outside this seeder) - in both cases
// the match legitimately fails, and business rule 3 says the uninstaller
// should report and skip, not delete something else that happens to look
// the same.
type SysAppCasbinGrant struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
AppCode string `json:"appCode" gorm:"type:varchar(64);not null;index:idx_sys_app_casbin_grant_app_code;comment:app code that created this grant"`
// Column widths mirror gorm-adapter's own CasbinRule struct exactly, so
// a value that fits into casbin_rule always fits here, and the unique
// index below matches the one createTable() puts on casbin_rule itself.
Ptype string `json:"ptype" gorm:"size:100;not null;uniqueIndex:uk_sys_app_casbin_grant_rule;comment:casbin ptype, 'p' today"`
V0 string `json:"v0" gorm:"size:100;not null;default:'';uniqueIndex:uk_sys_app_casbin_grant_rule;comment:role_key at grant time"`
V1 string `json:"v1" gorm:"size:100;not null;default:'';uniqueIndex:uk_sys_app_casbin_grant_rule;comment:api path"`
V2 string `json:"v2" gorm:"size:100;not null;default:'';uniqueIndex:uk_sys_app_casbin_grant_rule;comment:http method"`
V3 string `json:"v3" gorm:"size:100;not null;default:'';uniqueIndex:uk_sys_app_casbin_grant_rule;comment:unused today"`
V4 string `json:"v4" gorm:"size:100;not null;default:'';uniqueIndex:uk_sys_app_casbin_grant_rule;comment:unused today"`
V5 string `json:"v5" gorm:"size:100;not null;default:'';uniqueIndex:uk_sys_app_casbin_grant_rule;comment:unused today"`
CreatedAt time.Time `json:"createdAt" gorm:"comment:when this grant was recorded"`
}
func (*SysAppCasbinGrant) TableName() string {
return "sys_app_casbin_grant"
}
-29
View File
@@ -1,29 +0,0 @@
package models
import (
"go-admin/common/models"
)
type SysCategory struct {
models.Model
Name string `json:"name" gorm:"type:varchar(255);comment:名称"` //
Img string `json:"img" gorm:"type:varchar(255);comment:图标"` //
Sort int `json:"sort" gorm:"type:int(4);comment:排序"` //
Status int `json:"status" gorm:"type:int(1);comment:状态"` //
Remark string `json:"remark" gorm:"type:varchar(255);comment:备注"` //
models.ControlBy
models.ModelTime
}
func (SysCategory) TableName() string {
return "sys_category"
}
func (e *SysCategory) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SysCategory) GetId() interface{} {
return e.Id
}
-27
View File
@@ -1,27 +0,0 @@
package models
import (
"go-admin/common/models"
)
type SysChinaAreaData struct {
models.Model
PId string `json:"pId" gorm:"type:int(11);comment:上级编码"`
Name string `json:"name" gorm:"type:varchar(128);comment:名称"`
models.ControlBy
models.ModelTime
}
func (SysChinaAreaData) TableName() string {
return "sys_china_area_data"
}
func (e *SysChinaAreaData) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SysChinaAreaData) GetId() interface{} {
return e.Id
}
+30
View File
@@ -0,0 +1,30 @@
package models
import (
"go-admin/common/models"
)
type SysConfig struct {
models.Model
ConfigName string `json:"configName" gorm:"size:128;comment:ConfigName"` //
ConfigKey string `json:"configKey" gorm:"size:128;comment:ConfigKey"` //
ConfigValue string `json:"configValue" gorm:"size:255;comment:ConfigValue"` //
ConfigType string `json:"configType" gorm:"size:64;comment:ConfigType"`
IsFrontend string `json:"isFrontend" gorm:"size:64;comment:是否前台"` //
Remark string `json:"remark" gorm:"size:128;comment:Remark"` //
models.ControlBy
models.ModelTime
}
func (*SysConfig) TableName() string {
return "sys_config"
}
func (e *SysConfig) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SysConfig) GetId() interface{} {
return e.Id
}
-33
View File
@@ -1,33 +0,0 @@
package models
import (
"go-admin/common/models"
)
type SysContent struct {
models.Model
CateId int `json:"cateId" gorm:"type:int(11);comment:分类id"`
Name string `json:"name" gorm:"type:varchar(255);comment:名称"`
Status int `json:"status" gorm:"type:int(1);comment:状态"`
Img string `json:"img" gorm:"type:varchar(255);comment:图片"`
Content string `json:"content" gorm:"type:text;comment:内容"`
Remark string `json:"remark" gorm:"type:varchar(255);comment:备注"`
Sort int `json:"sort" gorm:"type:int(4);comment:排序"`
models.ControlBy
models.ModelTime
}
// TableName
func (SysContent) TableName() string {
return "sys_content"
}
// Generate
func (e *SysContent) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SysContent) GetId() interface{} {
return e.Id
}
@@ -1,4 +1,4 @@
package system package models
import "go-admin/common/models" import "go-admin/common/models"
@@ -7,11 +7,11 @@ type SysDept struct {
ParentId int `json:"parentId" gorm:""` //上级部门 ParentId int `json:"parentId" gorm:""` //上级部门
DeptPath string `json:"deptPath" gorm:"size:255;"` // DeptPath string `json:"deptPath" gorm:"size:255;"` //
DeptName string `json:"deptName" gorm:"size:128;"` //部门名称 DeptName string `json:"deptName" gorm:"size:128;"` //部门名称
Sort int `json:"sort" gorm:""` //排序 Sort int `json:"sort" gorm:"size:4;"` //排序
Leader string `json:"leader" gorm:"size:128;"` //负责人 Leader string `json:"leader" gorm:"size:128;"` //负责人
Phone string `json:"phone" gorm:"size:11;"` //手机 Phone string `json:"phone" gorm:"size:11;"` //手机
Email string `json:"email" gorm:"size:64;"` //邮箱 Email string `json:"email" gorm:"size:64;"` //邮箱
Status string `json:"status" gorm:"size:4;"` //状态 Status int `json:"status" gorm:"size:4;"` //状态
models.ControlBy models.ControlBy
models.ModelTime models.ModelTime
DataScope string `json:"dataScope" gorm:"-"` DataScope string `json:"dataScope" gorm:"-"`
@@ -19,7 +19,7 @@ type SysDept struct {
Children []SysDept `json:"children" gorm:"-"` Children []SysDept `json:"children" gorm:"-"`
} }
func (SysDept) TableName() string { func (*SysDept) TableName() string {
return "sys_dept" return "sys_dept"
} }
+34
View File
@@ -0,0 +1,34 @@
package models
import (
"go-admin/common/models"
)
type SysDictData struct {
DictCode int `json:"dictCode" gorm:"primaryKey;column:dict_code;autoIncrement;comment:主键编码"`
DictSort int `json:"dictSort" gorm:"size:20;comment:DictSort"`
DictLabel string `json:"dictLabel" gorm:"size:128;comment:DictLabel"`
DictValue string `json:"dictValue" gorm:"size:255;comment:DictValue"`
DictType string `json:"dictType" gorm:"size:64;comment:DictType"`
CssClass string `json:"cssClass" gorm:"size:128;comment:CssClass"`
ListClass string `json:"listClass" gorm:"size:128;comment:ListClass"`
IsDefault string `json:"isDefault" gorm:"size:8;comment:IsDefault"`
Status int `json:"status" gorm:"size:4;comment:Status"`
Default string `json:"default" gorm:"size:8;comment:Default"`
Remark string `json:"remark" gorm:"size:255;comment:Remark"`
models.ControlBy
models.ModelTime
}
func (*SysDictData) TableName() string {
return "sys_dict_data"
}
func (e *SysDictData) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SysDictData) GetId() interface{} {
return e.DictCode
}
@@ -1,21 +1,20 @@
package system package models
import ( import (
"go-admin/common/models" "go-admin/common/models"
) )
type SysDictType struct { type SysDictType struct {
ID int `json:"id" gorm:"primaryKey;column:dict_id;autoIncrement;comment:主键编码"`
DictName string `json:"dictName" gorm:"size:128;comment:DictName"`
DictType string `json:"dictType" gorm:"size:128;comment:DictType"`
Status int `json:"status" gorm:"size:4;comment:Status"`
Remark string `json:"remark" gorm:"size:255;comment:Remark"`
models.ControlBy models.ControlBy
models.ModelTime models.ModelTime
ID int `json:"id" gorm:"primaryKey;column:dict_id;autoIncrement;comment:主键编码"`
DictName string `json:"dictName" gorm:"type:varchar(128);comment:DictName"`
DictType string `json:"dictType" gorm:"type:varchar(128);comment:DictType"`
Status string `json:"status" gorm:"type:varchar(4);comment:Status"`
Remark string `json:"remark" gorm:"type:varchar(255);comment:Remark"`
} }
func (SysDictType) TableName() string { func (*SysDictType) TableName() string {
return "sys_dict_type" return "sys_dict_type"
} }
-40
View File
@@ -1,40 +0,0 @@
package models
import (
"go-admin/common/models"
)
type SysFileDir struct {
models.Model
Label string `json:"label" gorm:"type:varchar(255);comment:目录名称"` // 目录名称
PId int `json:"pId" gorm:"type:int(11);comment:上级目录"` // 上级目录
Sort string `json:"sort" gorm:"type:bigint(20);comment:排序"` // 排序
Path string `json:"path" gorm:"type:varchar(255);comment:路径"` // 路径
Children []SysFileDir `json:"children,omitempty" gorm:"-"` // 下级信息
models.ControlBy
models.ModelTime
}
type SysFileDirL struct {
models.Model
Label string `json:"label" gorm:"type:varchar(255);comment:目录名称"` // 目录名称
PId int `json:"pId" gorm:"type:int(11);comment:上级目录"` // 上级目录
Sort string `json:"sort" gorm:"type:bigint(20);comment:排序"` // 排序
Path string `json:"path" gorm:"type:varchar(255);comment:路径"` // 路径
models.ControlBy
models.ModelTime
Children []SysFileDirL `json:"children,omitempty" gorm:"-"` // 下级信息
}
func (SysFileDir) TableName() string { /**/
return "sys_file_dir"
}
func (e *SysFileDir) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SysFileDir) GetId() interface{} {
return e.Id
}
-31
View File
@@ -1,31 +0,0 @@
package models
import (
"go-admin/common/models"
)
type SysFileInfo struct {
models.Model
Type string `json:"type" gorm:"type:varchar(255);comment:类型"` //
Name string `json:"name" gorm:"type:varchar(255);comment:名称"` //
Size string `json:"size" gorm:"type:int(11);comment:大小"` //
PId int `json:"pId" gorm:"type:int(11);comment:目录"` //
Source string `json:"source" gorm:"type:varchar(255);comment:来源"` //
Url string `json:"url" gorm:"type:varchar(255);comment:地址"` //
FullUrl string `json:"fullUrl" gorm:"type:varchar(255);comment:全地址"` //
models.ControlBy
models.ModelTime
}
func (SysFileInfo) TableName() string {
return "sys_file_info"
}
func (e *SysFileInfo) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SysFileInfo) GetId() interface{} {
return e.Id
}
@@ -1,35 +1,35 @@
package system package models
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"time" "time"
log "github.com/go-admin-team/go-admin-core/logger" log "github.com/go-admin-team/go-admin-core/v2/logger"
"github.com/go-admin-team/go-admin-core/sdk" "github.com/go-admin-team/go-admin-core/v2/sdk"
"github.com/go-admin-team/go-admin-core/storage" "github.com/go-admin-team/go-admin-core/v2/storage"
"go-admin/common/models" "go-admin/common/models"
) )
type SysLoginLog struct { type SysLoginLog struct {
models.Model models.Model
Username string `json:"username" gorm:"type:varchar(128);comment:用户名"` Username string `json:"username" gorm:"size:128;comment:用户名"`
Status string `json:"status" gorm:"type:varchar(4);comment:状态"` Status string `json:"status" gorm:"size:4;comment:状态"`
Ipaddr string `json:"ipaddr" gorm:"type:varchar(255);comment:ip地址"` Ipaddr string `json:"ipaddr" gorm:"size:255;comment:ip地址"`
LoginLocation string `json:"loginLocation" gorm:"type:varchar(255);comment:归属地"` LoginLocation string `json:"loginLocation" gorm:"size:255;comment:归属地"`
Browser string `json:"browser" gorm:"type:varchar(255);comment:浏览器"` Browser string `json:"browser" gorm:"size:255;comment:浏览器"`
Os string `json:"os" gorm:"type:varchar(255);comment:系统"` Os string `json:"os" gorm:"size:255;comment:系统"`
Platform string `json:"platform" gorm:"type:varchar(255);comment:固件"` Platform string `json:"platform" gorm:"size:255;comment:固件"`
LoginTime time.Time `json:"loginTime" gorm:"type:timestamp;comment:登录时间"` LoginTime time.Time `json:"loginTime" gorm:"comment:登录时间"`
Remark string `json:"remark" gorm:"type:varchar(255);comment:备注"` Remark string `json:"remark" gorm:"size:255;comment:备注"`
Msg string `json:"msg" gorm:"type:varchar(255);comment:信息"` Msg string `json:"msg" gorm:"size:255;comment:信息"`
CreatedAt time.Time `json:"createdAt" gorm:"comment:创建时间"` CreatedAt time.Time `json:"createdAt" gorm:"comment:创建时间"`
UpdatedAt time.Time `json:"updatedAt" gorm:"comment:最后更新时间"` UpdatedAt time.Time `json:"updatedAt" gorm:"comment:最后更新时间"`
models.ControlBy models.ControlBy
} }
func (SysLoginLog) TableName() string { func (*SysLoginLog) TableName() string {
return "sys_login_log" return "sys_login_log"
} }
@@ -45,7 +45,7 @@ func (e *SysLoginLog) GetId() interface{} {
// SaveLoginLog 从队列中获取登录日志 // SaveLoginLog 从队列中获取登录日志
func SaveLoginLog(message storage.Messager) (err error) { func SaveLoginLog(message storage.Messager) (err error) {
//准备db //准备db
db := sdk.Runtime.GetDbByKey(message.GetPrefix()) db := sdk.Runtime.GetDbByTenant(message.GetPrefix())
if db == nil { if db == nil {
err = errors.New("db not exist") err = errors.New("db not exist")
log.Errorf("host[%s]'s %s", message.GetPrefix(), err.Error()) log.Errorf("host[%s]'s %s", message.GetPrefix(), err.Error())
+80
View File
@@ -0,0 +1,80 @@
package models
import "go-admin/common/models"
type SysMenu struct {
MenuId int `json:"menuId" gorm:"primaryKey;autoIncrement"`
MenuName string `json:"menuName" gorm:"size:128;"`
Title string `json:"title" gorm:"size:128;"`
Icon string `json:"icon" gorm:"size:128;"`
Path string `json:"path" gorm:"size:128;"`
Paths string `json:"paths" gorm:"size:128;"`
MenuType string `json:"menuType" gorm:"size:1;"`
Action string `json:"action" gorm:"size:16;"`
Permission string `json:"permission" gorm:"size:255;"`
ParentId int `json:"parentId" gorm:"size:11;"`
NoCache bool `json:"noCache" gorm:"size:8;"`
Breadcrumb string `json:"breadcrumb" gorm:"size:255;"`
Component string `json:"component" gorm:"size:255;"`
Sort int `json:"sort" gorm:"size:4;"`
Visible string `json:"visible" gorm:"size:1;"`
IsFrame string `json:"isFrame" gorm:"size:1;DEFAULT:0;"`
SysApi []SysApi `json:"sysApi" gorm:"many2many:sys_menu_api_rule"`
Apis []int `json:"apis" gorm:"-"`
DataScope string `json:"dataScope" gorm:"-"`
Params string `json:"params" gorm:"-"`
RoleId int `gorm:"-"`
Children []SysMenu `json:"children,omitempty" gorm:"-"`
IsSelect bool `json:"is_select" gorm:"-"`
// AppCode identifies which application's seed.SeedMenus call wrote this
// row; empty for the host's own built-in menus. NOT NULL DEFAULT '' for
// the same reason sys_migration.app_code is (see contract/models.Migration):
// AutoMigrate adding this column to an existing table leaves every
// pre-existing row reading back as "" rather than NULL.
AppCode string `json:"appCode" gorm:"type:varchar(64);not null;default:'';index:idx_sys_menu_app_code;comment:AppCode"`
// SeedCode is the raw seed.MenuSpec.Code this row was created from, kept
// so seedMenuTree can ask "did I already write this node" without
// relying on MenuName's PascalCase concatenation, which is not
// injective (see design doc §1.6). Nullable, unlike AppCode: every row
// seed.SeedMenus writes sets a real value, but every pre-existing row -
// the host's own hand-placed menus, and every app-seeded row written
// before this column existed - has none, and there is no way to
// backfill one that means anything. NULL is what lets an unbounded
// number of those coexist under the same app_code without tripping the
// unique index below: the database never treats two NULLs as equal, so
// only rows that do carry a real code participate in the uniqueness
// check at all.
// uk_sys_menu_app_seed_code_del is created by the migration, not from
// this tag, and deliberately: it covers (app_code, seed_code,
// deleted_at), and this struct cannot say so. A named uniqueIndex tag
// puts every field carrying that name into one index, and deleted_at
// comes from the shared ModelTime embed, which no single model can add a
// tag to. Naming it here anyway declared a unique index on seed_code
// alone under the same name - stricter than the real one, forbidding two
// applications from both having a "dir" node - and AutoMigrate on this
// model would have created that one first, after which the migration's
// HasIndex guard finds the name taken and leaves the wrong index in
// place.
SeedCode *string `json:"seedCode" gorm:"size:64;comment:raw MenuSpec.Code, null for rows not written through SeedMenus"`
models.ControlBy
models.ModelTime
}
type SysMenuSlice []SysMenu
func (x SysMenuSlice) Len() int { return len(x) }
func (x SysMenuSlice) Less(i, j int) bool { return x[i].Sort < x[j].Sort }
func (x SysMenuSlice) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (*SysMenu) TableName() string {
return "sys_menu"
}
func (e *SysMenu) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SysMenu) GetId() interface{} {
return e.MenuId
}
@@ -0,0 +1,49 @@
package models
import (
"testing"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func ptr(s string) *string { return &s }
// uk_sys_menu_app_seed_code_del covers (app_code, seed_code, deleted_at) and
// is created by 1786700008000, not from a struct tag. It cannot come from a
// tag: a named uniqueIndex collects every field carrying that name, and
// deleted_at lives in the shared ModelTime embed that no single model can tag.
//
// Naming it on SeedCode alone anyway produced a unique index on seed_code by
// itself under the same name - stricter than the real one - and AutoMigrate
// here would create that one, after which the migration's HasIndex guard
// finds the name taken and leaves the wrong index in place. Nothing in
// production AutoMigrates this model (the initial table migration uses a
// frozen snapshot that has neither column), which is why this never showed up
// as a broken database; it showed up the first time a test built the schema
// from the live model and seeded two applications.
func TestSysMenuDeclaresNoSeedCodeIndexOfItsOwn(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Fatalf("open: %v", err)
}
if err := db.AutoMigrate(&SysMenu{}); err != nil {
t.Fatalf("automigrate: %v", err)
}
if db.Migrator().HasIndex(&SysMenu{}, "uk_sys_menu_app_seed_code_del") {
t.Error("AutoMigrate created uk_sys_menu_app_seed_code_del from a tag; " +
"the migration's HasIndex guard will now skip the composite index it should create")
}
// Two applications, the same seed code. The real index allows it because
// app_code is part of the key; an index on seed_code alone does not.
for _, app := range []string{"order", "crm"} {
row := SysMenu{MenuName: app + "Dir", AppCode: app, SeedCode: ptr("dir")}
if err := db.Create(&row).Error; err != nil {
t.Fatalf("%s could not use the seed code \"dir\": %v", app, err)
}
}
}
+88
View File
@@ -0,0 +1,88 @@
package models
import (
"encoding/json"
"errors"
"time"
log "github.com/go-admin-team/go-admin-core/v2/logger"
"github.com/go-admin-team/go-admin-core/v2/sdk"
"github.com/go-admin-team/go-admin-core/v2/storage"
"go-admin/common/models"
)
type SysOperaLog struct {
models.Model
Title string `json:"title" gorm:"size:255;comment:操作模块"`
BusinessType string `json:"businessType" gorm:"size:128;comment:操作类型"`
BusinessTypes string `json:"businessTypes" gorm:"size:128;comment:BusinessTypes"`
Method string `json:"method" gorm:"size:128;comment:函数"`
RequestMethod string `json:"requestMethod" gorm:"size:128;comment:请求方式 GET POST PUT DELETE"`
OperatorType string `json:"operatorType" gorm:"size:128;comment:操作类型"`
OperName string `json:"operName" gorm:"size:128;comment:操作者"`
DeptName string `json:"deptName" gorm:"size:128;comment:部门名称"`
OperUrl string `json:"operUrl" gorm:"size:255;comment:访问地址"`
OperIp string `json:"operIp" gorm:"size:128;comment:客户端ip"`
OperLocation string `json:"operLocation" gorm:"size:128;comment:访问位置"`
OperParam string `json:"operParam" gorm:"text;comment:请求参数"`
Status string `json:"status" gorm:"size:4;comment:操作状态 1:正常 2:关闭"`
OperTime time.Time `json:"operTime" gorm:"comment:操作时间"`
JsonResult string `json:"jsonResult" gorm:"size:255;comment:返回数据"`
Remark string `json:"remark" gorm:"size:255;comment:备注"`
LatencyTime string `json:"latencyTime" gorm:"size:128;comment:耗时"`
UserAgent string `json:"userAgent" gorm:"size:255;comment:ua"`
CreatedAt time.Time `json:"createdAt" gorm:"comment:创建时间"`
UpdatedAt time.Time `json:"updatedAt" gorm:"comment:最后更新时间"`
models.ControlBy
}
func (*SysOperaLog) TableName() string {
return "sys_opera_log"
}
func (e *SysOperaLog) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SysOperaLog) GetId() interface{} {
return e.Id
}
// SaveOperaLog 从队列中获取操作日志
func SaveOperaLog(message storage.Messager) (err error) {
//准备db
db := sdk.Runtime.GetDbByTenant(message.GetPrefix())
if db == nil {
err = errors.New("db not exist")
log.Errorf("host[%s]'s %s", message.GetPrefix(), err.Error())
// Log writing to the database ignores error
return nil
}
var rb []byte
rb, err = json.Marshal(message.GetValues())
if err != nil {
log.Errorf("json Marshal error, %s", err.Error())
// Log writing to the database ignores error
return nil
}
var l SysOperaLog
err = json.Unmarshal(rb, &l)
if err != nil {
log.Errorf("json Unmarshal error, %s", err.Error())
// Log writing to the database ignores error
return nil
}
// 超出100个字符返回值截断
if len(l.JsonResult) > 100 {
l.JsonResult = l.JsonResult[:100]
}
err = db.Create(&l).Error
if err != nil {
log.Errorf("db create error, %s", err.Error())
// Log writing to the database ignores error
return nil
}
return nil
}
@@ -1,4 +1,4 @@
package system package models
import "go-admin/common/models" import "go-admin/common/models"
@@ -6,7 +6,7 @@ type SysPost struct {
PostId int `gorm:"primaryKey;autoIncrement" json:"postId"` //岗位编号 PostId int `gorm:"primaryKey;autoIncrement" json:"postId"` //岗位编号
PostName string `gorm:"size:128;" json:"postName"` //岗位名称 PostName string `gorm:"size:128;" json:"postName"` //岗位名称
PostCode string `gorm:"size:128;" json:"postCode"` //岗位代码 PostCode string `gorm:"size:128;" json:"postCode"` //岗位代码
Sort int `gorm:"" json:"sort"` //岗位排序 Sort int `gorm:"size:4;" json:"sort"` //岗位排序
Status int `gorm:"size:4;" json:"status"` //状态 Status int `gorm:"size:4;" json:"status"` //状态
Remark string `gorm:"size:255;" json:"remark"` //描述 Remark string `gorm:"size:255;" json:"remark"` //描述
models.ControlBy models.ControlBy
@@ -16,7 +16,7 @@ type SysPost struct {
Params string `gorm:"-" json:"params"` Params string `gorm:"-" json:"params"`
} }
func (SysPost) TableName() string { func (*SysPost) TableName() string {
return "sys_post" return "sys_post"
} }
+35
View File
@@ -0,0 +1,35 @@
package models
import "go-admin/common/models"
type SysRole struct {
RoleId int `json:"roleId" gorm:"primaryKey;autoIncrement"` // 角色编码
RoleName string `json:"roleName" gorm:"size:128;"` // 角色名称
Status string `json:"status" gorm:"size:4;"` // 状态 1禁用 2正常
RoleKey string `json:"roleKey" gorm:"size:128;"` //角色代码
RoleSort int `json:"roleSort" gorm:""` //角色排序
Flag string `json:"flag" gorm:"size:128;"` //
Remark string `json:"remark" gorm:"size:255;"` //备注
Admin bool `json:"admin" gorm:"size:4;"`
DataScope string `json:"dataScope" gorm:"size:128;"`
Params string `json:"params" gorm:"-"`
MenuIds []int `json:"menuIds" gorm:"-"`
DeptIds []int `json:"deptIds" gorm:"-"`
SysDept []SysDept `json:"sysDept" gorm:"many2many:sys_role_dept;foreignKey:RoleId;joinForeignKey:role_id;references:DeptId;joinReferences:dept_id;"`
SysMenu *[]SysMenu `json:"sysMenu" gorm:"many2many:sys_role_menu;foreignKey:RoleId;joinForeignKey:role_id;references:MenuId;joinReferences:menu_id;"`
models.ControlBy
models.ModelTime
}
func (*SysRole) TableName() string {
return "sys_role"
}
func (e *SysRole) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SysRole) GetId() interface{} {
return e.RoleId
}
+89
View File
@@ -0,0 +1,89 @@
package models
import (
"go-admin/common/models"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type SysUser struct {
UserId int `gorm:"primaryKey;autoIncrement;comment:编码" json:"userId"`
Username string `json:"username" gorm:"size:64;comment:用户名"`
Password string `json:"-" gorm:"size:128;comment:密码"`
NickName string `json:"nickName" gorm:"size:128;comment:昵称"`
Phone string `json:"phone" gorm:"size:11;comment:手机号"`
RoleId int `json:"roleId" gorm:"size:20;comment:角色ID"`
Salt string `json:"-" gorm:"size:255;comment:加盐"`
Avatar string `json:"avatar" gorm:"size:255;comment:头像"`
Sex string `json:"sex" gorm:"size:255;comment:性别"`
Email string `json:"email" gorm:"size:128;comment:邮箱"`
DeptId int `json:"deptId" gorm:"size:20;comment:部门"`
PostId int `json:"postId" gorm:"size:20;comment:岗位"`
Remark string `json:"remark" gorm:"size:255;comment:备注"`
Status string `json:"status" gorm:"size:4;comment:状态"`
DeptIds []int `json:"deptIds" gorm:"-"`
PostIds []int `json:"postIds" gorm:"-"`
RoleIds []int `json:"roleIds" gorm:"-"`
Dept *SysDept `json:"dept"`
models.ControlBy
models.ModelTime
}
func (*SysUser) TableName() string {
return "sys_user"
}
func (e *SysUser) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SysUser) GetId() interface{} {
return e.UserId
}
// Encrypt hashes Password, unless it already holds a hash.
//
// The hooks below run on whatever is in the struct, and a user read from the
// database carries the stored hash in that field. Hashing it again produces a
// hash of a hash, and the password that user knows no longer matches anything:
// they cannot log in, and nothing reports an error. The only thing preventing
// that today is an Omit("password") on the one update that loads a user first,
// which makes every other write to this model one line away from destroying
// credentials.
//
// bcrypt.Cost parses a hash and fails on anything else, so it distinguishes
// the two cases without the call site having to say which it is. The cost is
// that a password which is itself a well-formed bcrypt hash would be stored
// unchanged - a 60-character string beginning "$2a$", not something a person
// types, and it grants whoever set it no access they did not already have.
func (e *SysUser) Encrypt() error {
if e.Password == "" {
return nil
}
if _, err := bcrypt.Cost([]byte(e.Password)); err == nil {
return nil
}
hash, err := bcrypt.GenerateFromPassword([]byte(e.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
e.Password = string(hash)
return nil
}
func (e *SysUser) BeforeCreate(_ *gorm.DB) error {
return e.Encrypt()
}
func (e *SysUser) BeforeUpdate(_ *gorm.DB) error {
return e.Encrypt()
}
func (e *SysUser) AfterFind(_ *gorm.DB) error {
e.DeptIds = []int{e.DeptId}
e.PostIds = []int{e.PostId}
e.RoleIds = []int{e.RoleId}
return nil
}
+106
View File
@@ -0,0 +1,106 @@
package models
import (
"testing"
"golang.org/x/crypto/bcrypt"
)
const knownPassword = "correct-horse-battery-staple"
// A user loaded from the database carries the stored hash in Password, and the
// hooks run on whatever is in the struct. Hashing it a second time produces a
// hash of a hash: the password the user knows stops matching, they cannot log
// in, and nothing reports an error.
//
// Only an Omit("password") on one call site stood between this and every write
// to the model. This is the test that removes the need for it.
func TestEncryptLeavesAnAlreadyHashedPasswordAlone(t *testing.T) {
fresh := SysUser{Password: knownPassword}
if err := fresh.Encrypt(); err != nil {
t.Fatalf("Encrypt: %v", err)
}
stored := fresh.Password
if err := bcrypt.CompareHashAndPassword([]byte(stored), []byte(knownPassword)); err != nil {
t.Fatalf("setup failed: the password was not hashed: %v", err)
}
// What a query puts in the struct, and what an update then hands the hook.
loaded := SysUser{Password: stored}
if err := loaded.Encrypt(); err != nil {
t.Fatalf("Encrypt on a loaded user: %v", err)
}
if loaded.Password != stored {
t.Error("Encrypt re-hashed a stored hash; the user can no longer log in")
}
if err := bcrypt.CompareHashAndPassword([]byte(loaded.Password), []byte(knownPassword)); err != nil {
t.Errorf("the user can no longer log in with their password: %v", err)
}
}
// The other half: a password that is not a hash still gets hashed, on create
// and on update alike.
func TestEncryptHashesAPlaintextPassword(t *testing.T) {
for _, c := range []struct {
name string
hook func(*SysUser) error
}{
{"BeforeCreate", func(u *SysUser) error { return u.BeforeCreate(nil) }},
{"BeforeUpdate", func(u *SysUser) error { return u.BeforeUpdate(nil) }},
} {
t.Run(c.name, func(t *testing.T) {
u := SysUser{Password: knownPassword}
if err := c.hook(&u); err != nil {
t.Fatal(err)
}
if u.Password == knownPassword {
t.Fatal("the password was stored as it was typed")
}
if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(knownPassword)); err != nil {
t.Errorf("the stored value does not verify the password: %v", err)
}
})
}
}
// An empty Password means "not being set", and must not become a hash of "".
func TestEncryptIgnoresAnEmptyPassword(t *testing.T) {
u := SysUser{}
if err := u.Encrypt(); err != nil {
t.Fatal(err)
}
if u.Password != "" {
t.Errorf("an unset password became %q", u.Password)
}
}
// Encrypt runs on every update of this model, including the ones that change
// something else entirely. What it costs when there is nothing to do is the
// difference between a profile update and a bcrypt round; the correctness test
// above is what catches a regression, this reports the size of it.
func BenchmarkEncrypt(b *testing.B) {
fresh := SysUser{Password: knownPassword}
if err := fresh.Encrypt(); err != nil {
b.Fatal(err)
}
b.Run("already hashed", func(b *testing.B) {
u := SysUser{Password: fresh.Password}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if err := u.Encrypt(); err != nil {
b.Fatal(err)
}
}
})
b.Run("plaintext", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
u := SysUser{Password: knownPassword}
if err := u.Encrypt(); err != nil {
b.Fatal(err)
}
}
})
}
-44
View File
@@ -1,44 +0,0 @@
package models
import (
"go-admin/common/models"
)
type SysSetting struct {
SettingsId int `json:"settings_id" gorm:"primary_key;AUTO_INCREMENT"`
Name string `json:"name" gorm:"type:varchar(256);"`
Logo string `json:"logo" gorm:"type:varchar(256);"`
models.ModelTime
}
func (SysSetting) TableName() string {
return "sys_setting"
}
func (s *SysSetting) GetId() interface{} {
return s.SettingsId
}
//查询
//func (s *SysSetting) Get() (create SysSetting, err error) {
// result := orm.Eloquent.Table("sys_setting").First(&create)
// if result.Error != nil {
// err = result.Error
// return
// }
// return create, nil
//}
//修改
//func (s *SysSetting) Update() (update SysSetting, err error) {
// if err = orm.Eloquent.Table("sys_setting").Model(&update).Where("settings_id = ?", s.SettingsId).Updates(&s).Error; err != nil {
// return
// }
// return
//}
type ResponseSystemConfig struct {
Name string `json:"name" binding:"required"` // 名称
Logo string `json:"logo" binding:"required"` // 头像
SettingsId int `json:"settings_id" binding:"required"` // 头像
}
-16
View File
@@ -1,16 +0,0 @@
package system
//sys_casbin_rule
type CasbinRule struct {
PType string `json:"p_type" gorm:"size:100;"`
V0 string `json:"v0" gorm:"size:100;"`
V1 string `json:"v1" gorm:"size:100;"`
V2 string `json:"v2" gorm:"size:100;"`
V3 string `json:"v3" gorm:"size:100;"`
V4 string `json:"v4" gorm:"size:100;"`
V5 string `json:"v5" gorm:"size:100;"`
}
func (CasbinRule) TableName() string {
return "sys_casbin_rule"
}
-81
View File
@@ -1,81 +0,0 @@
package system
import (
"errors"
"github.com/go-admin-team/go-admin-core/sdk/pkg"
"gorm.io/gorm"
log "github.com/go-admin-team/go-admin-core/logger"
"github.com/go-admin-team/go-admin-core/sdk/config"
)
type DataPermission struct {
DataScope string
UserId int
DeptId int
RoleId int
}
func (e *DataPermission) GetDataScope(tableName string, db *gorm.DB) (*gorm.DB, error) {
if !config.ApplicationConfig.EnableDP {
usageStr := `数据权限已经为您` + pkg.Green(`关闭`) + `,如需开启请参考配置文件字段说明`
log.Debug("%s\n", usageStr)
return db, nil
}
user := new(SysUser)
role := new(SysRole)
err := db.Find(user, e.UserId).Error
if err != nil {
return nil, errors.New("获取用户数据出错 msg:" + err.Error())
}
err = db.Find(role, user.RoleId).Error
if err != nil {
return nil, errors.New("获取用户数据出错 msg:" + err.Error())
}
if role.DataScope == "2" {
db = db.Where(tableName+".create_by in (select sys_user.user_id from sys_role_dept left join sys_user on sys_user.dept_id=sys_role_dept.dept_id where sys_role_dept.role_id = ?)", user.RoleId)
}
if role.DataScope == "3" {
db = db.Where(tableName+".create_by in (SELECT user_id from sys_user where dept_id = ? )", user.DeptId)
}
if role.DataScope == "4" {
db = db.Where(tableName+".create_by in (SELECT user_id from sys_user where sys_user.dept_id in(select dept_id from sys_dept where dept_path like ? ))", "%"+pkg.IntToString(user.DeptId)+"%")
}
if role.DataScope == "5" || role.DataScope == "" {
db = db.Where(tableName+".create_by = ?", e.UserId)
}
return db, nil
}
func DataScopes(tableName string, userId int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
user := new(SysUser)
role := new(SysRole)
user.UserId = userId
err := db.Find(user, userId).Error
if err != nil {
db.Error = errors.New("获取用户数据出错 msg:" + err.Error())
return db
}
err = db.Find(role, user.RoleId).Error
if err != nil {
db.Error = errors.New("获取用户数据出错 msg:" + err.Error())
return db
}
if role.DataScope == "2" {
return db.Where(tableName+".create_by in (select sys_user.user_id from sys_role_dept left join sys_user on sys_user.dept_id=sys_role_dept.dept_id where sys_role_dept.role_id = ?)", user.RoleId)
}
if role.DataScope == "3" {
return db.Where(tableName+".create_by in (SELECT user_id from sys_user where dept_id = ? )", user.DeptId)
}
if role.DataScope == "4" {
return db.Where(tableName+".create_by in (SELECT user_id from sys_user where sys_user.dept_id in(select dept_id from sys_dept where dept_path like ? ))", "%"+pkg.IntToString(user.DeptId)+"%")
}
if role.DataScope == "5" || role.DataScope == "" {
return db.Where(tableName+".create_by = ?", userId)
}
return db
}
}
-11
View File
@@ -1,11 +0,0 @@
package system
//sys_role_dept
type SysRoleDept struct {
RoleId int `gorm:"size:11;primaryKey"`
DeptId int `gorm:"size:11;primaryKey"`
}
func (SysRoleDept) TableName() string {
return "sys_role_dept"
}
-172
View File
@@ -1,172 +0,0 @@
package system
import (
"fmt"
"github.com/casbin/casbin/v2"
"github.com/go-admin-team/go-admin-core/sdk/pkg"
"gorm.io/gorm"
"go-admin/common/models"
)
type RoleMenu struct {
RoleId int `gorm:""`
MenuId int `gorm:""`
RoleName string `gorm:"size:128"`
models.ControlBy
}
func (RoleMenu) TableName() string {
return "sys_role_menu"
}
type MenuPath struct {
Path string `json:"path"`
}
func (rm *RoleMenu) Get(tx *gorm.DB) ([]RoleMenu, error) {
var r []RoleMenu
table := tx.Table("sys_role_menu")
if rm.RoleId != 0 {
table = table.Where("role_id = ?", rm.RoleId)
}
if err := table.Find(&r).Error; err != nil {
return nil, err
}
return r, nil
}
func (rm *RoleMenu) GetPermis(tx *gorm.DB) ([]string, error) {
var r []SysMenu
table := tx.Select("sys_menu.permission").Table("sys_menu").Joins("left join sys_role_menu on sys_menu.menu_id = sys_role_menu.menu_id")
table = table.Where("role_id = ?", rm.RoleId)
table = table.Where("sys_menu.menu_type in('F','C')")
if err := table.Find(&r).Error; err != nil {
return nil, err
}
var list []string
for i := 0; i < len(r); i++ {
list = append(list, r[i].Permission)
}
return list, nil
}
func (rm *RoleMenu) GetIDS(tx *gorm.DB) ([]MenuPath, error) {
var r []MenuPath
table := tx.Select("sys_menu.path").Table("sys_role_menu")
table = table.Joins("left join sys_role on sys_role.role_id=sys_role_menu.role_id")
table = table.Joins("left join sys_menu on sys_menu.id=sys_role_menu.menu_id")
table = table.Where("sys_role.role_name = ? and sys_menu.type=1", rm.RoleName)
if err := table.Find(&r).Error; err != nil {
return nil, err
}
return r, nil
}
func (rm *RoleMenu) DeleteRoleMenu(tx *gorm.DB, roleId int) error {
if err := tx.Table("sys_role_dept").Where("role_id = ?", roleId).Delete(&rm).Error; err != nil {
return err
}
if err := tx.Table("sys_role_menu").Where("role_id = ?", roleId).Delete(&rm).Error; err != nil {
return err
}
var role SysRole
if err := tx.Table("sys_role").Where("role_id = ?", roleId).First(&role).Error; err != nil {
return err
}
sql3 := "delete from sys_casbin_rule where v0= '" + role.RoleKey + "';"
if err := tx.Exec(sql3).Error; err != nil {
return err
}
return nil
}
// 该方法即将弃用
func (rm *RoleMenu) BatchDeleteRoleMenu(tx *gorm.DB, roleIds []int) error {
if err := tx.Table("sys_role_menu").Where("role_id in (?)", roleIds).Delete(&rm).Error; err != nil {
return err
}
var role []SysRole
if err := tx.Table("sys_role").Where("role_id in (?)", roleIds).Find(&role).Error; err != nil {
return err
}
sql := ""
for i := 0; i < len(role); i++ {
sql += "delete from sys_casbin_rule where v0= '" + role[i].RoleName + "';"
}
if err := tx.Exec(sql).Error; err != nil {
return err
}
if err := tx.Commit().Error; err != nil {
return err
}
return nil
}
func (rm *RoleMenu) Insert(tx *gorm.DB, enforcer *casbin.SyncedEnforcer, roleId int, menuId []int) error {
var err error
var (
role SysRole
menu []SysMenu
casbinRules []CasbinRule // casbinRule 待插入队列
)
// 在事务中做一些数据库操作(从这一点使用'tx',而不是'db'
if err = tx.Table("sys_role").Where("role_id = ?", roleId).First(&role).Error; err != nil {
return err
}
if err = tx.Table("sys_menu").Where("menu_id in (?)", menuId).Find(&menu).Error; err != nil {
return err
}
//ORM不支持批量插入所以需要拼接 sql 串
sysRoleMenuSql := "INSERT INTO `sys_role_menu` (`role_id`,`menu_id`,`role_name`) VALUES "
for i, m := range menu {
// 拼装'role_menu'表批量插入SQL语句
sysRoleMenuSql += fmt.Sprintf("(%d,%d,'%s')", role.RoleId, m.MenuId, role.RoleKey)
if i == len(menu)-1 {
sysRoleMenuSql += ";" //最后一条数据 以分号结尾
} else {
sysRoleMenuSql += ","
}
if m.MenuType == "A" {
// 加入队列
casbinRules = append(casbinRules,
CasbinRule{
V0: role.RoleKey,
V1: m.Path,
V2: m.Action,
})
}
}
// 执行批量插入sys_role_menu
if err = tx.Exec(sysRoleMenuSql).Error; err != nil {
return err
}
// 执行批量插入sys_casbin_rule
if len(casbinRules) > 0 {
if err = tx.Create(&casbinRules).Error; err != nil {
return err
}
}
return nil
}
func (rm *RoleMenu) Delete(tx *gorm.DB, RoleId string, MenuID string) (bool, error) {
rm.RoleId, _ = pkg.StringToInt(RoleId)
table := tx.Table("sys_role_menu").Where("role_id = ?", RoleId)
if MenuID != "" {
table = table.Where("menu_id = ?", MenuID)
}
if err := table.Delete(&rm).Error; err != nil {
return false, err
}
return true, nil
}
-30
View File
@@ -1,30 +0,0 @@
package system
import (
"go-admin/common/models"
)
type SysConfig struct {
models.Model
ConfigName string `json:"configName" gorm:"type:varchar(128);comment:ConfigName"` //
ConfigKey string `json:"configKey" gorm:"type:varchar(128);comment:ConfigKey"` //
ConfigValue string `json:"configValue" gorm:"type:varchar(255);comment:ConfigValue"` //
ConfigType string `json:"configType" gorm:"type:varchar(64);comment:ConfigType"`
IsFrontend int `json:"isFrontend" gorm:"type:varchar(64);comment:是否前台"` //
Remark string `json:"remark" gorm:"type:varchar(128);comment:Remark"` //
models.ControlBy
models.ModelTime
}
func (SysConfig) TableName() string {
return "sys_config"
}
func (e *SysConfig) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SysConfig) GetId() interface{} {
return e.Id
}
-35
View File
@@ -1,35 +0,0 @@
package system
import (
"go-admin/common/models"
)
type SysDictData struct {
models.ControlBy
models.ModelTime
DictCode int `json:"dictCode" gorm:"primaryKey;column:dict_code;autoIncrement;comment:主键编码"`
DictSort int `json:"dictSort" gorm:"type:bigint(20);comment:DictSort"`
DictLabel string `json:"dictLabel" gorm:"type:varchar(128);comment:DictLabel"`
DictValue string `json:"dictValue" gorm:"type:varchar(255);comment:DictValue"`
DictType string `json:"dictType" gorm:"type:varchar(64);comment:DictType"`
CssClass string `json:"cssClass" gorm:"type:varchar(128);comment:CssClass"`
ListClass string `json:"listClass" gorm:"type:varchar(128);comment:ListClass"`
IsDefault string `json:"isDefault" gorm:"type:varchar(8);comment:IsDefault"`
Status string `json:"status" gorm:"type:varchar(4);comment:Status"`
Default string `json:"default" gorm:"type:varchar(8);comment:Default"`
Remark string `json:"remark" gorm:"type:varchar(255);comment:Remark"`
}
func (SysDictData) TableName() string {
return "sys_dict_data"
}
func (e *SysDictData) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SysDictData) GetId() interface{} {
return e.DictCode
}
-64
View File
@@ -1,64 +0,0 @@
package system
import "go-admin/common/models"
type SysMenu struct {
MenuId int `json:"menuId" gorm:"primaryKey;autoIncrement"`
MenuName string `json:"menuName" gorm:"size:128;"`
Title string `json:"title" gorm:"size:128;"`
Icon string `json:"icon" gorm:"size:128;"`
Path string `json:"path" gorm:"size:128;"`
Paths string `json:"paths" gorm:"size:128;"`
MenuType string `json:"menuType" gorm:"size:1;"`
Action string `json:"action" gorm:"size:16;"`
Permission string `json:"permission" gorm:"size:255;"`
ParentId int `json:"parentId" gorm:"size:11;"`
NoCache bool `json:"noCache" gorm:"size:8;"`
Breadcrumb string `json:"breadcrumb" gorm:"size:255;"`
Component string `json:"component" gorm:"size:255;"`
Sort int `json:"sort" gorm:"size:4;"`
Visible string `json:"visible" gorm:"size:1;"`
IsFrame string `json:"isFrame" gorm:"size:1;DEFAULT:0;"`
DataScope string `json:"dataScope" gorm:"-"`
Params string `json:"params" gorm:"-"`
RoleId int `gorm:"-"`
Children []SysMenu `json:"children" gorm:"-"`
IsSelect bool `json:"is_select" gorm:"-"`
models.ControlBy
models.ModelTime
}
type SysMenus struct {
MenuId int `json:"menuId" gorm:"column:menu_id;primaryKey;autoIncrement;"`
MenuName string `json:"menuName" gorm:"column:menu_name"`
Title string `json:"title" gorm:"column:title"`
Icon string `json:"icon" gorm:"column:icon"`
Path string `json:"path" gorm:"column:path"`
MenuType string `json:"menuType" gorm:"column:menu_type"`
Action string `json:"action" gorm:"column:action"`
Permission string `json:"permission" gorm:"column:permission"`
ParentId int `json:"parentId" gorm:"column:parent_id"`
NoCache bool `json:"noCache" gorm:"column:no_cache"`
Breadcrumb string `json:"breadcrumb" gorm:"column:breadcrumb"`
Component string `json:"component" gorm:"column:component"`
Sort int `json:"sort" gorm:"column:sort"`
Visible string `json:"visible" gorm:"column:visible"`
Children []SysMenu `json:"children" gorm:"-"`
models.ControlBy
models.ModelTime
DataScope string `json:"dataScope" gorm:"-"`
Params string `json:"params" gorm:"-"`
}
func (SysMenu) TableName() string {
return "sys_menu"
}
func (e *SysMenu) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SysMenu) GetId() interface{} {
return e.MenuId
}
-85
View File
@@ -1,85 +0,0 @@
package system
import (
"encoding/json"
"errors"
"time"
log "github.com/go-admin-team/go-admin-core/logger"
"github.com/go-admin-team/go-admin-core/sdk"
"github.com/go-admin-team/go-admin-core/storage"
"go-admin/common/models"
)
type SysOperaLog struct {
models.Model
Title string `json:"title" gorm:"type:varchar(255);comment:操作模块"`
BusinessType string `json:"businessType" gorm:"type:varchar(128);comment:操作类型"`
BusinessTypes string `json:"businessTypes" gorm:"type:varchar(128);comment:BusinessTypes"`
Method string `json:"method" gorm:"type:varchar(128);comment:函数"`
RequestMethod string `json:"requestMethod" gorm:"type:varchar(128);comment:请求方式"`
OperatorType string `json:"operatorType" gorm:"type:varchar(128);comment:操作类型"`
OperName string `json:"operName" gorm:"type:varchar(128);comment:操作者"`
DeptName string `json:"deptName" gorm:"type:varchar(128);comment:部门名称"`
OperUrl string `json:"operUrl" gorm:"type:varchar(255);comment:访问地址"`
OperIp string `json:"operIp" gorm:"type:varchar(128);comment:客户端ip"`
OperLocation string `json:"operLocation" gorm:"type:varchar(128);comment:访问位置"`
OperParam string `json:"operParam" gorm:"type:varchar(255);comment:请求参数"`
Status string `json:"status" gorm:"type:varchar(4);comment:操作状态"`
OperTime time.Time `json:"operTime" gorm:"type:timestamp;comment:操作时间"`
JsonResult string `json:"jsonResult" gorm:"type:varchar(255);comment:返回数据"`
Remark string `json:"remark" gorm:"type:varchar(255);comment:备注"`
LatencyTime string `json:"latencyTime" gorm:"type:varchar(128);comment:耗时"`
UserAgent string `json:"userAgent" gorm:"type:varchar(255);comment:ua"`
CreatedAt time.Time `json:"createdAt" gorm:"comment:创建时间"`
UpdatedAt time.Time `json:"updatedAt" gorm:"comment:最后更新时间"`
models.ControlBy
}
func (SysOperaLog) TableName() string {
return "sys_opera_log"
}
func (e *SysOperaLog) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SysOperaLog) GetId() interface{} {
return e.Id
}
// SaveOperaLog 从队列中获取操作日志
func SaveOperaLog(message storage.Messager) (err error) {
//准备db
db := sdk.Runtime.GetDbByKey(message.GetPrefix())
if db == nil {
err = errors.New("db not exist")
log.Errorf("host[%s]'s %s", message.GetPrefix(), err.Error())
return err
}
var rb []byte
rb, err = json.Marshal(message.GetValues())
if err != nil {
log.Errorf("json Marshal error, %s", err.Error())
return err
}
var l SysOperaLog
err = json.Unmarshal(rb, &l)
if err != nil {
log.Errorf("json Unmarshal error, %s", err.Error())
return err
}
if l.Title == "" {
m := &SysMenu{}
db.Model(m).Select("Title").Where("action = ?", l.Method).Where("path = ?", message.GetValues()["_fullPath"]).First(m)
l.Title = m.Title
}
err = db.Create(&l).Error
if err != nil {
log.Errorf("db create error, %s", err.Error())
return err
}
return nil
}
-33
View File
@@ -1,33 +0,0 @@
package system
import "go-admin/common/models"
type SysRole struct {
RoleId int `json:"roleId" gorm:"primaryKey;autoIncrement"` // 角色编码
RoleName string `json:"roleName" gorm:"size:128;"` // 角色名称
Status string `json:"status" gorm:"size:4;"` //
RoleKey string `json:"roleKey" gorm:"size:128;"` //角色代码
RoleSort int `json:"roleSort" gorm:""` //角色排序
Flag string `json:"flag" gorm:"size:128;"` //
Remark string `json:"remark" gorm:"size:255;"` //备注
Admin bool `json:"admin" gorm:"size:4;"`
DataScope string `json:"dataScope" gorm:"size:128;"`
Params string `json:"params" gorm:"-"`
MenuIds []int `json:"menuIds" gorm:"-"`
DeptIds []int `json:"deptIds" gorm:"-"`
models.ControlBy
models.ModelTime
}
func (SysRole) TableName() string {
return "sys_role"
}
func (e *SysRole) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SysRole) GetId() interface{} {
return e.RoleId
}
-77
View File
@@ -1,77 +0,0 @@
package system
import (
"go-admin/common/models"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type SysUser struct {
models.ControlBy
models.ModelTime
UserId int `gorm:"primaryKey;autoIncrement;comment:编码" json:"userId"`
Username string `json:"username" gorm:"type:varchar(64);comment:用户名"`
Password string `json:"-" gorm:"type:varchar(128);comment:密码"`
NickName string `json:"nickName" gorm:"type:varchar(128);comment:昵称"`
Phone string `json:"phone" gorm:"type:varchar(11);comment:手机号"`
RoleId int `json:"roleId" gorm:"type:bigint(20);comment:角色ID"`
Salt string `json:"-" gorm:"type:varchar(255);comment:加盐"`
Avatar string `json:"avatar" gorm:"type:varchar(255);comment:头像"`
Sex string `json:"sex" gorm:"type:varchar(255);comment:性别"`
Email string `json:"email" gorm:"type:varchar(128);comment:邮箱"`
DeptId int `json:"deptId" gorm:"type:bigint(20);comment:部门"`
PostId int `json:"postId" gorm:"type:bigint(20);comment:岗位"`
Remark string `json:"remark" gorm:"type:varchar(255);comment:备注"`
Status string `json:"status" gorm:"type:varchar(4);comment:状态"`
DeptIds []int `json:"deptIds" gorm:"-"`
PostIds []int `json:"postIds" gorm:"-"`
RoleIds []int `json:"roleIds" gorm:"-"`
Dept *SysDept `json:"dept"`
}
func (SysUser) TableName() string {
return "sys_user"
}
func (e *SysUser) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *SysUser) GetId() interface{} {
return e.UserId
}
//加密
func (e *SysUser) Encrypt() (err error) {
if e.Password == "" {
return
}
var hash []byte
if hash, err = bcrypt.GenerateFromPassword([]byte(e.Password), bcrypt.DefaultCost); err != nil {
return
} else {
e.Password = string(hash)
return
}
}
func (e *SysUser) BeforeCreate(_ *gorm.DB) error {
return e.Encrypt()
}
func (e *SysUser) BeforeUpdate(_ *gorm.DB) error {
var err error
if e.Password != "" {
err = e.Encrypt()
}
return err
}
func (e *SysUser) AfterFind(_ *gorm.DB) error {
e.DeptIds = []int{e.DeptId}
e.PostIds = []int{e.PostId}
e.RoleIds = []int{e.RoleId}
return nil
}
-62
View File
@@ -1,62 +0,0 @@
package tools
import (
"errors"
"github.com/go-admin-team/go-admin-core/sdk/pkg"
"gorm.io/gorm"
config2 "github.com/go-admin-team/go-admin-core/sdk/config"
)
type DBTables struct {
TableName string `gorm:"column:TABLE_NAME" json:"tableName"`
Engine string `gorm:"column:ENGINE" json:"engine"`
TableRows string `gorm:"column:TABLE_ROWS" json:"tableRows"`
TableCollation string `gorm:"column:TABLE_COLLATION" json:"tableCollation"`
CreateTime string `gorm:"column:CREATE_TIME" json:"createTime"`
UpdateTime string `gorm:"column:UPDATE_TIME" json:"updateTime"`
TableComment string `gorm:"column:TABLE_COMMENT" json:"tableComment"`
}
func (e *DBTables) GetPage(tx *gorm.DB, pageSize int, pageIndex int) ([]DBTables, int, error) {
var doc []DBTables
table := new(gorm.DB)
var count int64
if config2.DatabaseConfig.Driver == "mysql" {
table = tx.Table("information_schema.tables")
table = table.Where("TABLE_NAME not in (select table_name from `" + config2.GenConfig.DBName + "`.sys_tables) ")
table = table.Where("table_schema= ? ", config2.GenConfig.DBName)
if e.TableName != "" {
table = table.Where("TABLE_NAME = ?", e.TableName)
}
if err := table.Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&doc).Offset(-1).Limit(-1).Count(&count).Error; err != nil {
return nil, 0, err
}
} else {
pkg.Assert(true, "目前只支持mysql数据库", 500)
}
//table.Count(&count)
return doc, int(count), nil
}
func (e *DBTables) Get(tx *gorm.DB) (DBTables, error) {
var doc DBTables
if config2.DatabaseConfig.Driver == "mysql" {
table := tx.Table("information_schema.tables")
table = table.Where("table_schema= ? ", config2.GenConfig.DBName)
if e.TableName == "" {
return doc, errors.New("table name cannot be empty")
}
table = table.Where("TABLE_NAME = ?", e.TableName)
if err := table.First(&doc).Error; err != nil {
return doc, err
}
} else {
pkg.Assert(true, "目前只支持mysql数据库", 500)
}
return doc, nil
}
+7 -21
View File
@@ -4,14 +4,9 @@ import (
"os" "os"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
log "github.com/go-admin-team/go-admin-core/logger" log "github.com/go-admin-team/go-admin-core/v2/logger"
"github.com/go-admin-team/go-admin-core/sdk" "github.com/go-admin-team/go-admin-core/v2/sdk"
"github.com/go-admin-team/go-admin-core/sdk/api"
"github.com/go-admin-team/go-admin-core/sdk/config"
"github.com/go-admin-team/go-admin-core/sdk/pkg"
common "go-admin/common/middleware" common "go-admin/common/middleware"
"go-admin/common/middleware/handler"
) )
// InitRouter 路由初始化,不要怀疑,这里用到了 // InitRouter 路由初始化,不要怀疑,这里用到了
@@ -19,8 +14,8 @@ func InitRouter() {
var r *gin.Engine var r *gin.Engine
h := sdk.Runtime.GetEngine() h := sdk.Runtime.GetEngine()
if h == nil { if h == nil {
h = gin.New() log.Fatal("not found engine...")
sdk.Runtime.SetEngine(h) os.Exit(-1)
} }
switch h.(type) { switch h.(type) {
case *gin.Engine: case *gin.Engine:
@@ -29,19 +24,10 @@ func InitRouter() {
log.Fatal("not support other engine") log.Fatal("not support other engine")
os.Exit(-1) os.Exit(-1)
} }
if config.SslConfig.Enable {
r.Use(handler.TlsHandler())
}
r.Use(common.Sentinel()). // the jwt middleware: shared instance InitMiddleware built at startup,
Use(common.RequestId(pkg.TrafficKey)). // not one built here per module (see common/middleware.GetAuthMiddleware).
Use(api.SetRequestLogger) authMiddleware := common.GetAuthMiddleware()
common.InitMiddleware(r)
// the jwt middleware
authMiddleware, err := common.AuthInit()
if err != nil {
log.Fatalf("JWT Init Error, %s", err.Error())
}
// 注册系统路由 // 注册系统路由
InitSysRouter(r, authMiddleware) InitSysRouter(r, authMiddleware)
-32
View File
@@ -1,32 +0,0 @@
package router
import (
"github.com/go-admin-team/go-admin-core/sdk"
"net/http"
"github.com/gin-gonic/gin"
log "github.com/go-admin-team/go-admin-core/logger"
"github.com/go-admin-team/go-admin-core/tools/transfer"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func Monitor() {
var r *gin.Engine
h := sdk.Runtime.GetEngine()
if h == nil {
h = gin.New()
sdk.Runtime.SetEngine(h)
}
switch h.(type) {
case *gin.Engine:
r = h.(*gin.Engine)
default:
log.Fatal("not support other engine")
}
//开发环境启动监控指标
r.GET("/metrics", transfer.Handler(promhttp.Handler()))
//健康检查
r.GET("/health", func(c *gin.Context) {
c.Status(http.StatusOK)
})
}
+2 -12
View File
@@ -3,8 +3,8 @@ package router
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
_ "github.com/gin-gonic/gin" _ "github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth" "github.com/go-admin-team/go-admin-core/v2/jwtauth"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth" jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
) )
var ( var (
@@ -12,7 +12,6 @@ var (
routerCheckRole = make([]func(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware), 0) routerCheckRole = make([]func(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware), 0)
) )
// 路由示例
func InitExamplesRouter(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware) *gin.Engine { func InitExamplesRouter(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware) *gin.Engine {
// 无需认证的路由 // 无需认证的路由
@@ -27,24 +26,15 @@ func InitExamplesRouter(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware) *gi
func examplesNoCheckRoleRouter(r *gin.Engine) { func examplesNoCheckRoleRouter(r *gin.Engine) {
// 可根据业务需求来设置接口版本 // 可根据业务需求来设置接口版本
v1 := r.Group("/api/v1") v1 := r.Group("/api/v1")
// 空接口防止v1定义无使用报错
v1.GET("/nilcheckrole", nil)
for _, f := range routerNoCheckRole { for _, f := range routerNoCheckRole {
f(v1) f(v1)
} }
// {{无需认证路由自动补充在此处请勿删除}}
//registerSysFileInfoRouter(v1)
} }
// 需要认证的路由示例 // 需要认证的路由示例
func examplesCheckRoleRouter(r *gin.Engine, authMiddleware *jwtauth.GinJWTMiddleware) { func examplesCheckRoleRouter(r *gin.Engine, authMiddleware *jwtauth.GinJWTMiddleware) {
// 可根据业务需求来设置接口版本 // 可根据业务需求来设置接口版本
v1 := r.Group("/api/v1") v1 := r.Group("/api/v1")
// 空接口防止v1定义无使用报错
v1.GET("/checkrole", nil)
for _, f := range routerCheckRole { for _, f := range routerCheckRole {
f(v1, authMiddleware) f(v1, authMiddleware)
} }
+28
View File
@@ -0,0 +1,28 @@
package router
import (
"github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
"go-admin/app/admin/apis"
"go-admin/common/actions"
"go-admin/common/middleware"
)
func init() {
routerCheckRole = append(routerCheckRole, registerSysApiRouter)
}
// registerSysApiRouter
func registerSysApiRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := apis.SysApi{}
// PermissionAction is not optional here: all three handlers below read the
// data permission out of the context, and without it they read the zero
// value - an unset scope, which Permission now fails closed on.
r := v1.Group("/sys-api").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole()).Use(actions.PermissionAction())
{
r.GET("", api.GetPage)
r.GET("/:id", api.Get)
r.PUT("/:id", api.Update)
}
}
-31
View File
@@ -1,31 +0,0 @@
package router
import (
"github.com/gin-gonic/gin"
middleware2 "go-admin/common/middleware"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
"go-admin/app/admin/models"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
)
func init() {
routerCheckRole = append(routerCheckRole, registerSysCategoryRouter)
}
// 需认证的路由代码
func registerSysCategoryRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
r := v1.Group("/syscategory").Use(authMiddleware.MiddlewareFunc()).Use(middleware2.AuthCheckRole())
{
model := &models.SysCategory{}
r.GET("", actions.PermissionAction(), actions.IndexAction(model, new(dto.SysCategorySearch), func() interface{} {
list := make([]models.SysCategory, 0)
return &list
}))
r.GET("/:id", actions.PermissionAction(), actions.ViewAction(new(dto.SysCategoryById), nil))
r.POST("", actions.CreateAction(new(dto.SysCategoryControl)))
r.PUT("/:id", actions.PermissionAction(), actions.UpdateAction(new(dto.SysCategoryControl)))
r.DELETE("", actions.PermissionAction(), actions.DeleteAction(new(dto.SysCategoryById)))
}
}
-25
View File
@@ -1,25 +0,0 @@
package router
import (
"github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
"go-admin/app/admin/apis/sys_china_area_data"
middleware2 "go-admin/common/middleware"
)
func init() {
routerCheckRole = append(routerCheckRole, registerSysChinaAreaDataRouter)
}
// 需认证的路由代码
func registerSysChinaAreaDataRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := sys_china_area_data.SysChinaAreaData{}
r := v1.Group("/sys_china_area_data").Use(authMiddleware.MiddlewareFunc()).Use(middleware2.AuthCheckRole())
{
r.GET("", api.GetSysChinaAreaDataList)
r.GET("/:id", api.GetSysChinaAreaData)
r.POST("", api.InsertSysChinaAreaData)
r.PUT("/:id", api.UpdateSysChinaAreaData)
r.DELETE("", api.DeleteSysChinaAreaData)
}
}
+19 -13
View File
@@ -1,10 +1,11 @@
package router package router
import ( import (
"go-admin/app/admin/apis"
"go-admin/common/middleware"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth" jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
"go-admin/app/admin/apis/system/sys_config"
middleware2 "go-admin/common/middleware"
) )
func init() { func init() {
@@ -13,15 +14,14 @@ func init() {
// 需认证的路由代码 // 需认证的路由代码
func registerSysConfigRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) { func registerSysConfigRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := sys_config.SysConfig{} api := apis.SysConfig{}
r := v1.Group("/config").Use(authMiddleware.MiddlewareFunc()).Use(middleware2.AuthCheckRole()) r := v1.Group("/config").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
{ {
r.GET("", api.GetPage)
r.GET("", api.GetSysConfigList) r.GET("/:id", api.Get)
r.GET("/:id", api.GetSysConfig) r.POST("", api.Insert)
r.POST("", api.InsertSysConfig) r.PUT("/:id", api.Update)
r.PUT("/:id", api.UpdateSysConfig) r.DELETE("", api.Delete)
r.DELETE("", api.DeleteSysConfig)
} }
r1 := v1.Group("/configKey").Use(authMiddleware.MiddlewareFunc()) r1 := v1.Group("/configKey").Use(authMiddleware.MiddlewareFunc())
@@ -31,7 +31,13 @@ func registerSysConfigRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMidd
r2 := v1.Group("/app-config") r2 := v1.Group("/app-config")
{ {
r2.GET("", api.GetSysConfigBySysApp) r2.GET("", api.Get2SysApp)
} }
} r3 := v1.Group("/set-config").Use(authMiddleware.MiddlewareFunc())
{
r3.PUT("", api.Update2Set)
r3.GET("", api.Get2Set)
}
}
-37
View File
@@ -1,37 +0,0 @@
package router
import (
"github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
"go-admin/app/admin/models"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
middleware2 "go-admin/common/middleware"
)
func init() {
routerCheckRole = append(routerCheckRole, registerSysContentRouter)
}
// 需认证的路由代码
func registerSysContentRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
r := v1.Group("/syscontent").Use(authMiddleware.MiddlewareFunc()).Use(middleware2.AuthCheckRole())
{
//r.GET("", sys_content.GetSysContentList)
//r.GET("/:id", sys_content.GetSysContent)
//r.POST("", sys_content.InsertSysContent)
//r.PUT("", sys_content.UpdateSysContent)
//r.DELETE("/:id", sys_content.DeleteSysContent)
model := &models.SysContent{}
r.GET("", actions.PermissionAction(), actions.IndexAction(model, new(dto.SysContentSearch), func() interface{} {
list := make([]models.SysContent, 0)
return &list
}))
r.GET("/:id", actions.PermissionAction(), actions.ViewAction(new(dto.SysContentById), nil))
r.POST("", actions.CreateAction(new(dto.SysContentControl)))
r.PUT("/:id", actions.PermissionAction(), actions.UpdateAction(new(dto.SysContentControl)))
r.DELETE("", actions.PermissionAction(), actions.DeleteAction(new(dto.SysContentById)))
}
}
+12 -11
View File
@@ -2,9 +2,9 @@ package router
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth" jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
"go-admin/app/admin/apis/system/sys_dept" "go-admin/app/admin/apis"
middleware2 "go-admin/common/middleware" "go-admin/common/middleware"
) )
func init() { func init() {
@@ -13,19 +13,20 @@ func init() {
// 需认证的路由代码 // 需认证的路由代码
func registerSysDeptRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) { func registerSysDeptRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := sys_dept.SysDept{} api := apis.SysDept{}
r := v1.Group("/dept").Use(authMiddleware.MiddlewareFunc()).Use(middleware2.AuthCheckRole())
r := v1.Group("/dept").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
{ {
r.GET("", api.GetSysDeptList) r.GET("", api.GetPage)
r.GET("/:id", api.GetSysDept) r.GET("/:id", api.Get)
r.POST("", api.InsertSysDept) r.POST("", api.Insert)
r.PUT("/:id", api.UpdateSysDept) r.PUT("/:id", api.Update)
r.DELETE("/:id", api.DeleteSysDept) r.DELETE("", api.Delete)
} }
r1 := v1.Group("").Use(authMiddleware.MiddlewareFunc()) r1 := v1.Group("").Use(authMiddleware.MiddlewareFunc())
{ {
r1.GET("/deptTree", api.GetDeptTree) r1.GET("/deptTree", api.Get2Tree)
} }
} }
+37
View File
@@ -0,0 +1,37 @@
package router
import (
"github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
"go-admin/app/admin/apis"
"go-admin/common/middleware"
)
func init() {
routerCheckRole = append(routerCheckRole, registerDictRouter)
}
func registerDictRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
dictApi := apis.SysDictType{}
dataApi := apis.SysDictData{}
dicts := v1.Group("/dict").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
{
dicts.GET("/data", dataApi.GetPage)
dicts.GET("/data/:dictCode", dataApi.Get)
dicts.POST("/data", dataApi.Insert)
dicts.PUT("/data/:dictCode", dataApi.Update)
dicts.DELETE("/data", dataApi.Delete)
dicts.GET("/type-option-select", dictApi.GetAll)
dicts.GET("/type", dictApi.GetPage)
dicts.GET("/type/:id", dictApi.Get)
dicts.POST("/type", dictApi.Insert)
dicts.PUT("/type/:id", dictApi.Update)
dicts.DELETE("/type", dictApi.Delete)
}
opSelect := v1.Group("/dict-data").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
{
opSelect.GET("/option-select", dataApi.GetAll)
}
}
-25
View File
@@ -1,25 +0,0 @@
package router
import (
"github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
"go-admin/app/admin/apis/sys_file"
middleware2 "go-admin/common/middleware"
)
func init() {
routerCheckRole = append(routerCheckRole, registerSysFileDirRouter)
}
// 需认证的路由代码
func registerSysFileDirRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := sys_file.SysFileDir{}
r := v1.Group("/sysfiledir").Use(authMiddleware.MiddlewareFunc()).Use(middleware2.AuthCheckRole())
{
r.GET("", api.GetSysFileDirList)
r.GET("/:id", api.GetSysFileDir)
r.POST("", api.InsertSysFileDir)
r.PUT("/:id", api.UpdateSysFileDir)
r.DELETE("/:id", api.DeleteSysFileDir)
}
}
-25
View File
@@ -1,25 +0,0 @@
package router
import (
"github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
"go-admin/app/admin/apis/sys_file"
middleware2 "go-admin/common/middleware"
)
func init() {
routerCheckRole = append(routerCheckRole, registerSysFileInfoRouter)
}
// 需认证的路由代码
func registerSysFileInfoRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := sys_file.SysFileInfo{}
r := v1.Group("/sysfileinfo").Use(authMiddleware.MiddlewareFunc()).Use(middleware2.AuthCheckRole())
{
r.GET("", api.GetSysFileInfoList)
r.GET("/:id", api.GetSysFileInfo)
r.POST("", api.InsertSysFileInfo)
r.PUT("/:id", api.UpdateSysFileInfo)
r.DELETE("/:id", api.DeleteSysFileInfo)
}
}
+9 -10
View File
@@ -2,9 +2,9 @@ package router
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth" jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
"go-admin/app/admin/apis/system/sys_login_log" "go-admin/app/admin/apis"
middleware2 "go-admin/common/middleware" "go-admin/common/middleware"
) )
func init() { func init() {
@@ -13,13 +13,12 @@ func init() {
// 需认证的路由代码 // 需认证的路由代码
func registerSysLoginLogRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) { func registerSysLoginLogRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := sys_login_log.SysLoginLog{} api := apis.SysLoginLog{}
r := v1.Group("/sys-login-log").Use(authMiddleware.MiddlewareFunc()).Use(middleware2.AuthCheckRole())
r := v1.Group("/sys-login-log").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
{ {
r.GET("", api.GetSysLoginLogList) r.GET("", api.GetPage)
r.GET("/:id", api.GetSysLoginLog) r.GET("/:id", api.Get)
r.POST("", api.InsertSysLoginLog) r.DELETE("", api.Delete)
r.PUT("/:id", api.UpdateSysLoginLog)
r.DELETE("", api.DeleteSysLoginLog)
} }
} }
+12 -18
View File
@@ -2,9 +2,9 @@ package router
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth" jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
"go-admin/app/admin/apis/system/sys_menu" "go-admin/app/admin/apis"
middleware2 "go-admin/common/middleware" "go-admin/common/middleware"
) )
func init() { func init() {
@@ -13,27 +13,21 @@ func init() {
// 需认证的路由代码 // 需认证的路由代码
func registerSysMenuRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) { func registerSysMenuRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := sys_menu.SysMenu{} api := apis.SysMenu{}
//menu := v1.Group("/menu").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
//{ r := v1.Group("/menu").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
// menu.GET("/:id", system.GetMenu)
// menu.POST("", system.InsertMenu)
// menu.PUT("", system.UpdateMenu)
// menu.DELETE("/:id", system.DeleteMenu)
//}
r := v1.Group("/menu").Use(authMiddleware.MiddlewareFunc()).Use(middleware2.AuthCheckRole())
{ {
r.GET("", api.GetSysMenuList) r.GET("", api.GetPage)
r.GET("/:id", api.GetSysMenu) r.GET("/:id", api.Get)
r.POST("", api.InsertSysMenu) r.POST("", api.Insert)
r.PUT("/:id", api.UpdateSysMenu) r.PUT("/:id", api.Update)
r.DELETE("/:id", api.DeleteSysMenu) r.DELETE("", api.Delete)
} }
r1 := v1.Group("").Use(authMiddleware.MiddlewareFunc()) r1 := v1.Group("").Use(authMiddleware.MiddlewareFunc())
{ {
r1.GET("/menurole", api.GetMenuRole) r1.GET("/menurole", api.GetMenuRole)
r1.GET("/menuids", api.GetMenuIDS) //r1.GET("/menuids", api.GetMenuIDS)
} }
} }

Some files were not shown because too many files have changed in this diff Show More